From 38bca3cfdea2877b6238074d3fd361e5f1d18f3d Mon Sep 17 00:00:00 2001 From: chrishavlin Date: Tue, 28 Mar 2023 12:41:20 -0500 Subject: [PATCH 01/12] scaffolding out a simple dataset store --- src/yt_napari/_ds_cache.py | 47 +++++++++++++++++++++++++++ src/yt_napari/_model_ingestor.py | 4 +-- src/yt_napari/_tests/test_ds_cache.py | 32 ++++++++++++++++++ 3 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 src/yt_napari/_ds_cache.py create mode 100644 src/yt_napari/_tests/test_ds_cache.py diff --git a/src/yt_napari/_ds_cache.py b/src/yt_napari/_ds_cache.py new file mode 100644 index 0000000..12a4fb9 --- /dev/null +++ b/src/yt_napari/_ds_cache.py @@ -0,0 +1,47 @@ +import weakref + +import yt + +from yt_napari.logging import ytnapari_log + + +class DatasetCache: + def __init__(self): + self.available = {} + self._most_recent: str = None + + def add_ds(self, ds, name: str): + if name in self.available: + ytnapari_log.warning(f"A dataset already exists for {name}. Overwriting.") + self.available[name] = weakref.proxy(ds) + self._most_recent = name + + @property + def most_recent(self): + if self._most_recent is not None: + return self.available[self._most_recent] + return None + + def get_ds(self, name: str): + if self.exists(name): + return self.available[name] + ytnapari_log.warning(f"{name} not found in cache.") + return None + + def exists(self, name: str): + return name in self.available + + def rm_ds(self, name: str): + self.available.pop(name, None) + + def rm_all(self): + self.available = {} + + def check_then_load(self, filename: str): + if self.exists(filename) is False: + ds = yt.load(filename) + self.add_ds(ds, filename) + return self.get_ds(filename) + + +dataset_cache = DatasetCache() diff --git a/src/yt_napari/_model_ingestor.py b/src/yt_napari/_model_ingestor.py index 0674ad3..dd666a4 100644 --- a/src/yt_napari/_model_ingestor.py +++ b/src/yt_napari/_model_ingestor.py @@ -1,10 +1,10 @@ from typing import List, Optional, Tuple, Union import numpy as np -import yt from unyt import unit_object, unit_registry, unyt_array from yt_napari._data_model import InputModel +from yt_napari._ds_cache import dataset_cache def _le_re_to_cen_wid( @@ -288,7 +288,7 @@ def _process_validated_model(model: InputModel) -> List[SpatialLayer]: # dataset and return a plain numpy array for m_data in model.data: - ds = yt.load(m_data.filename) + ds = dataset_cache.check_then_load(m_data.filename) for sel in m_data.selections: # get the left, right edge as a unitful array, initialize the layer diff --git a/src/yt_napari/_tests/test_ds_cache.py b/src/yt_napari/_tests/test_ds_cache.py new file mode 100644 index 0000000..6dd0805 --- /dev/null +++ b/src/yt_napari/_tests/test_ds_cache.py @@ -0,0 +1,32 @@ +from yt import testing as yt_testing + +from yt_napari._ds_cache import dataset_cache + + +def get_new_ds(): + return yt_testing.fake_amr_ds(fields=("density", "mass"), units=("kg/m**3", "kg")) + + +def test_ds_cache(caplog): + ds = get_new_ds() + ds_name = "test_name" + dataset_cache.add_ds(ds, ds_name) + assert dataset_cache.exists(ds_name) + assert dataset_cache.exists("test_name_bad") is False + assert dataset_cache._most_recent == ds_name + + dataset_cache.add_ds(ds, ds_name) + assert "A dataset already exists" in caplog.text + + ds_from_store = dataset_cache.get_ds(ds_name) + assert ds_from_store == ds + ds_from_store = dataset_cache.most_recent + assert ds_from_store == ds + + dataset_cache.rm_ds(ds_name) + assert dataset_cache.exists(ds_name) is False + assert len(dataset_cache.available) == 0 + + ds_none = dataset_cache.get_ds("doesnotexist") + assert ds_none is None + assert "doesnotexist not found in cache" in caplog.text From c71e92879e4a6106a3bd1cf942ac5d329e54e79a Mon Sep 17 00:00:00 2001 From: chrishavlin Date: Tue, 28 Mar 2023 14:59:33 -0500 Subject: [PATCH 02/12] log message when reading from cache --- src/yt_napari/_ds_cache.py | 2 ++ src/yt_napari/_tests/test_widget_reader.py | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/yt_napari/_ds_cache.py b/src/yt_napari/_ds_cache.py index 12a4fb9..d1ea359 100644 --- a/src/yt_napari/_ds_cache.py +++ b/src/yt_napari/_ds_cache.py @@ -41,6 +41,8 @@ def check_then_load(self, filename: str): if self.exists(filename) is False: ds = yt.load(filename) self.add_ds(ds, filename) + else: + ytnapari_log.info(f"loading {filename} from cache.") return self.get_ds(filename) diff --git a/src/yt_napari/_tests/test_widget_reader.py b/src/yt_napari/_tests/test_widget_reader.py index 6eee221..af6386f 100644 --- a/src/yt_napari/_tests/test_widget_reader.py +++ b/src/yt_napari/_tests/test_widget_reader.py @@ -5,7 +5,7 @@ from yt_napari._widget_reader import ReaderWidget -def test_widget_reader(make_napari_viewer, yt_ugrid_ds_fn): +def test_widget_reader(make_napari_viewer, yt_ugrid_ds_fn, caplog): # make_napari_viewer is a pytest fixture. It takes any keyword arguments # that napari.Viewer() takes. The fixture takes care of teardown, do **not** @@ -37,7 +37,8 @@ def rebuild_data(final_shape, data): r.data_container.selections.left_edge.value = (0.4, 0.4, 0.4) r.data_container.selections.right_edge.value = (0.6, 0.6, 0.6) r.load_data() - + # should have read from cache, check the log: + assert yt_ugrid_ds_fn in caplog.text # the viewer should now have two images assert len(viewer.layers) == 2 From 3622b7999a28c2998e030033777b342fcd81b959 Mon Sep 17 00:00:00 2001 From: chrishavlin Date: Tue, 28 Mar 2023 15:35:44 -0500 Subject: [PATCH 03/12] add some explicit reference checking --- src/yt_napari/_ds_cache.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/yt_napari/_ds_cache.py b/src/yt_napari/_ds_cache.py index d1ea359..e051968 100644 --- a/src/yt_napari/_ds_cache.py +++ b/src/yt_napari/_ds_cache.py @@ -28,9 +28,20 @@ def get_ds(self, name: str): ytnapari_log.warning(f"{name} not found in cache.") return None - def exists(self, name: str): + def exists(self, name: str) -> bool: return name in self.available + def reference_exists(self, name: str) -> bool: + if self.exists(name): + ds = self.get_ds(name) + ref_exists = True + try: + _ = ds.basename + except ReferenceError: + ref_exists = False + return ref_exists + return False + def rm_ds(self, name: str): self.available.pop(name, None) @@ -38,12 +49,13 @@ def rm_all(self): self.available = {} def check_then_load(self, filename: str): - if self.exists(filename) is False: + if self.reference_exists(filename): + ytnapari_log.info(f"loading {filename} from cache.") + return self.get_ds(filename) + else: ds = yt.load(filename) self.add_ds(ds, filename) - else: - ytnapari_log.info(f"loading {filename} from cache.") - return self.get_ds(filename) + return ds dataset_cache = DatasetCache() From 47d6e5885e84e3600f65f5b991299d318b6204e9 Mon Sep 17 00:00:00 2001 From: Chris Havlin Date: Tue, 28 Mar 2023 16:04:37 -0500 Subject: [PATCH 04/12] force a reference error in a test --- src/yt_napari/_ds_cache.py | 1 + src/yt_napari/_tests/test_ds_cache.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/yt_napari/_ds_cache.py b/src/yt_napari/_ds_cache.py index e051968..a7f2be1 100644 --- a/src/yt_napari/_ds_cache.py +++ b/src/yt_napari/_ds_cache.py @@ -47,6 +47,7 @@ def rm_ds(self, name: str): def rm_all(self): self.available = {} + self._most_recent = None def check_then_load(self, filename: str): if self.reference_exists(filename): diff --git a/src/yt_napari/_tests/test_ds_cache.py b/src/yt_napari/_tests/test_ds_cache.py index 6dd0805..01907a6 100644 --- a/src/yt_napari/_tests/test_ds_cache.py +++ b/src/yt_napari/_tests/test_ds_cache.py @@ -30,3 +30,20 @@ def test_ds_cache(caplog): ds_none = dataset_cache.get_ds("doesnotexist") assert ds_none is None assert "doesnotexist not found in cache" in caplog.text + + dataset_cache.add_ds(ds, ds_name) + assert dataset_cache.exists(ds_name) + dataset_cache.rm_all() + assert len(dataset_cache.available) == 0 + assert dataset_cache.most_recent is None + + +def _add_to_cache_then_delete(): + ds = get_new_ds() + dataset_cache.add_ds(ds, "hellotest") + del ds + + +def test_weakref_destruction(): + _add_to_cache_then_delete() + assert dataset_cache.reference_exists("hellotest") is False From 91d1668b337c9be1ecf62ec2ef0f38484c666ce0 Mon Sep 17 00:00:00 2001 From: chrishavlin Date: Mon, 3 Apr 2023 16:39:28 -0500 Subject: [PATCH 05/12] add config file, add store_in_cache option to model (and schema) --- docs/_static/yt-napari_0.0.2.json | 152 +++++++++++++++++++++ docs/_static/yt-napari_latest.json | 6 + docs/schema.rst | 2 + repo_utilities/update_schema_docs.py | 12 ++ requirements.txt | 1 + setup.cfg | 2 + src/yt_napari/_data_model.py | 5 + src/yt_napari/_ds_cache.py | 4 +- src/yt_napari/_tests/test_config.py | 25 ++++ src/yt_napari/_tests/test_ds_cache.py | 9 ++ src/yt_napari/config.py | 61 +++++++++ src/yt_napari/schemas/yt-napari_0.0.2.json | 152 +++++++++++++++++++++ 12 files changed, 430 insertions(+), 1 deletion(-) create mode 100644 docs/_static/yt-napari_0.0.2.json create mode 100644 src/yt_napari/_tests/test_config.py create mode 100644 src/yt_napari/config.py create mode 100644 src/yt_napari/schemas/yt-napari_0.0.2.json diff --git a/docs/_static/yt-napari_0.0.2.json b/docs/_static/yt-napari_0.0.2.json new file mode 100644 index 0000000..17be7b8 --- /dev/null +++ b/docs/_static/yt-napari_0.0.2.json @@ -0,0 +1,152 @@ +{ + "title": "InputModel", + "type": "object", + "properties": { + "data": { + "title": "Data", + "description": "list of datasets to load", + "type": "array", + "items": { + "$ref": "#/definitions/DataContainer" + } + } + }, + "definitions": { + "ytField": { + "title": "ytField", + "type": "object", + "properties": { + "field_type": { + "title": "Field Type", + "description": "a field type in the yt dataset", + "type": "string" + }, + "field_name": { + "title": "Field Name", + "description": "a field in the yt dataset", + "type": "string" + }, + "take_log": { + "title": "Take Log", + "description": "if true, will apply log10 to the selected data", + "default": true, + "type": "boolean" + } + } + }, + "SelectionObject": { + "title": "SelectionObject", + "type": "object", + "properties": { + "fields": { + "title": "Fields", + "description": "list of fields to load for this selection", + "type": "array", + "items": { + "$ref": "#/definitions/ytField" + } + }, + "left_edge": { + "title": "Left Edge", + "description": "the left edge (min x, min y, min z) in units of edge_units", + "default": [ + 0.0, + 0.0, + 0.0 + ], + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + } + ] + }, + "right_edge": { + "title": "Right Edge", + "description": "the right edge (max x, max y, max z) in units of edge_units", + "default": [ + 1.0, + 1.0, + 1.0 + ], + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + } + ] + }, + "resolution": { + "title": "Resolution", + "description": "the resolution at which to sample between the edges.", + "default": [ + 400, + 400, + 400 + ], + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "type": "integer" + }, + { + "type": "integer" + }, + { + "type": "integer" + } + ] + } + } + }, + "DataContainer": { + "title": "DataContainer", + "type": "object", + "properties": { + "filename": { + "title": "Filename", + "description": "the filename for the dataset", + "type": "string" + }, + "selections": { + "title": "Selections", + "description": "list of selections to load in this dataset", + "type": "array", + "items": { + "$ref": "#/definitions/SelectionObject" + } + }, + "edge_units": { + "title": "Edge Units", + "description": "the units to use for left_edge and right_edge in the selections", + "default": "code_length", + "type": "string" + }, + "store_in_cache": { + "title": "Store In Cache", + "description": "if enabled, will store references to yt datasets to avoid reloading.", + "default": true, + "type": "boolean" + } + } + } + } +} diff --git a/docs/_static/yt-napari_latest.json b/docs/_static/yt-napari_latest.json index 2ea693c..17be7b8 100644 --- a/docs/_static/yt-napari_latest.json +++ b/docs/_static/yt-napari_latest.json @@ -139,6 +139,12 @@ "description": "the units to use for left_edge and right_edge in the selections", "default": "code_length", "type": "string" + }, + "store_in_cache": { + "title": "Store In Cache", + "description": "if enabled, will store references to yt datasets to avoid reloading.", + "default": true, + "type": "boolean" } } } diff --git a/docs/schema.rst b/docs/schema.rst index de538a5..aa57324 100644 --- a/docs/schema.rst +++ b/docs/schema.rst @@ -17,4 +17,6 @@ The following versions are available (latest first): .. schemalistanchor! the following table is auto-generated by repo_utilites/update_schema_docs.py, Do not edit below this line. +yt-napari_0.0.2.json : `view <_static/yt-napari_0.0.2.json>`_ , :download:`download <_static/yt-napari_0.0.2.json>` + yt-napari_0.0.1.json : `view <_static/yt-napari_0.0.1.json>`_ , :download:`download <_static/yt-napari_0.0.1.json>` diff --git a/repo_utilities/update_schema_docs.py b/repo_utilities/update_schema_docs.py index f66df37..cddfe82 100644 --- a/repo_utilities/update_schema_docs.py +++ b/repo_utilities/update_schema_docs.py @@ -1,11 +1,23 @@ # copies over schema files to the docs/_static folder and updates the schema.rst +import argparse + +from yt_napari._data_model import _store_schema from yt_napari.schemas._manager import Manager def run_update(source_dir, schema_dir): + m = Manager(schema_dir) m.update_docs(source_dir) if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "-v", "--version", type=str, default=None, help="the schema version to write" + ) + + args = parser.parse_args() + if args.version is not None: + _store_schema(version=args.version, overwrite_version=True) run_update("./docs", "./src/yt_napari/schemas") diff --git a/requirements.txt b/requirements.txt index 66ce455..9e3b71a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ sphinx-jsonschema<1.19.0 Jinja2<3.1.0 magicgui pytest-qt +platformdirs -e . diff --git a/setup.cfg b/setup.cfg index 9313df7..a4ac876 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,6 +36,8 @@ install_requires = npe2 numpy pydantic + tomli + tomli-w yt>=4.0.1 python_requires = >=3.8 package_dir = diff --git a/src/yt_napari/_data_model.py b/src/yt_napari/_data_model.py index 67452dd..d08e5c9 100644 --- a/src/yt_napari/_data_model.py +++ b/src/yt_napari/_data_model.py @@ -4,6 +4,7 @@ from pydantic import BaseModel, Field +from yt_napari.config import ytnapari_config from yt_napari.schemas import _manager @@ -42,6 +43,10 @@ class DataContainer(BaseModel): "code_length", description="the units to use for left_edge and right_edge in the selections", ) + store_in_cache: Optional[bool] = Field( + ytnapari_config.get_option("in_memory_cache"), + description="if enabled, will store references to yt datasets.", + ) class InputModel(BaseModel): diff --git a/src/yt_napari/_ds_cache.py b/src/yt_napari/_ds_cache.py index a7f2be1..2f1eb9e 100644 --- a/src/yt_napari/_ds_cache.py +++ b/src/yt_napari/_ds_cache.py @@ -2,6 +2,7 @@ import yt +from yt_napari.config import ytnapari_config from yt_napari.logging import ytnapari_log @@ -55,7 +56,8 @@ def check_then_load(self, filename: str): return self.get_ds(filename) else: ds = yt.load(filename) - self.add_ds(ds, filename) + if ytnapari_config.get_option("in_memory_cache"): + self.add_ds(ds, filename) return ds diff --git a/src/yt_napari/_tests/test_config.py b/src/yt_napari/_tests/test_config.py new file mode 100644 index 0000000..65c04af --- /dev/null +++ b/src/yt_napari/_tests/test_config.py @@ -0,0 +1,25 @@ +import os + +import pytest + +from yt_napari.config import _ConfigContainer + + +def test_config(tmp_path): + + config_dir = str(tmp_path / "configdir") + custom_config = _ConfigContainer(config_dir=config_dir) + custom_config.set_option("in_memory_cache", False) # default is True + + config_file = custom_config.config_file + assert os.path.isfile(config_file) + + # load as new config object to make sure it updates from the on-disk config + custom_config = _ConfigContainer(config_dir=config_dir) + assert custom_config.get_option("in_memory_cache") is False + + with pytest.raises(KeyError, match="bad_key is not a valid option"): + custom_config.set_option("bad_key", False) + + with pytest.raises(KeyError, match="bad_key is not a valid option"): + custom_config.get_option("bad_key") diff --git a/src/yt_napari/_tests/test_ds_cache.py b/src/yt_napari/_tests/test_ds_cache.py index 01907a6..a758657 100644 --- a/src/yt_napari/_tests/test_ds_cache.py +++ b/src/yt_napari/_tests/test_ds_cache.py @@ -1,6 +1,7 @@ from yt import testing as yt_testing from yt_napari._ds_cache import dataset_cache +from yt_napari.config import ytnapari_config def get_new_ds(): @@ -47,3 +48,11 @@ def _add_to_cache_then_delete(): def test_weakref_destruction(): _add_to_cache_then_delete() assert dataset_cache.reference_exists("hellotest") is False + + +def test_config_option(yt_ugrid_ds_fn): + dataset_cache.rm_all() + ytnapari_config.set_option("in_memory_cache", False) + _ = dataset_cache.check_then_load(yt_ugrid_ds_fn) + assert yt_ugrid_ds_fn not in dataset_cache.available + ytnapari_config.set_option("in_memory_cache", True) diff --git a/src/yt_napari/config.py b/src/yt_napari/config.py new file mode 100644 index 0000000..0888e1e --- /dev/null +++ b/src/yt_napari/config.py @@ -0,0 +1,61 @@ +import os + +import platformdirs +import tomli +import tomli_w + +from yt_napari.logging import ytnapari_log + +_defaults = {"in_memory_cache": True} + + +class _ConfigContainer: + def __init__(self, config_dir=None): + if config_dir is None: + app_dirs = platformdirs.AppDirs("yt-napari") + self.dir = app_dirs.user_config_dir + else: + self.dir = config_dir + self.config_file_name = "yt-napari.yaml" + self.config_file = os.path.join(self.dir, self.config_file_name) + self.config_dict = {} + self.load_config() + + def load_config(self): + if os.path.isfile(self.config_file): + with open(self.config_file, "rb") as fi: + config_dict = tomli.load(fi) + else: + config_dict = {} + + self.config_dict = _defaults.copy() + self.config_dict.update(config_dict) + + def set_option(self, option: str, value): + if option not in _defaults: + raise KeyError(f"{option} is not a valid option") + + self.config_dict[option] = value + self.write_to_disk() + + def write_to_disk(self): + if os.path.exists(self.dir) is False: + try: + os.makedirs(self.dir, exist_ok=True) + except OSError: + ytnapari_log.warning(f"Could not create {self.dir}") + + try: + with open(self.config_file, "wb") as fi: + tomli_w.dump(self.config_dict, fi) + except OSError: + ytnapari_log.warning(f"Could not write {self.config_file}") + + def get_option(self, option: str): + if option not in self.config_dict: + raise KeyError(f"{option} is not a valid option") + + return self.config_dict[option] + + +ytnapari_config = _ConfigContainer() diff --git a/src/yt_napari/schemas/yt-napari_0.0.2.json b/src/yt_napari/schemas/yt-napari_0.0.2.json new file mode 100644 index 0000000..17be7b8 --- /dev/null +++ b/src/yt_napari/schemas/yt-napari_0.0.2.json @@ -0,0 +1,152 @@ +{ + "title": "InputModel", + "type": "object", + "properties": { + "data": { + "title": "Data", + "description": "list of datasets to load", + "type": "array", + "items": { + "$ref": "#/definitions/DataContainer" + } + } + }, + "definitions": { + "ytField": { + "title": "ytField", + "type": "object", + "properties": { + "field_type": { + "title": "Field Type", + "description": "a field type in the yt dataset", + "type": "string" + }, + "field_name": { + "title": "Field Name", + "description": "a field in the yt dataset", + "type": "string" + }, + "take_log": { + "title": "Take Log", + "description": "if true, will apply log10 to the selected data", + "default": true, + "type": "boolean" + } + } + }, + "SelectionObject": { + "title": "SelectionObject", + "type": "object", + "properties": { + "fields": { + "title": "Fields", + "description": "list of fields to load for this selection", + "type": "array", + "items": { + "$ref": "#/definitions/ytField" + } + }, + "left_edge": { + "title": "Left Edge", + "description": "the left edge (min x, min y, min z) in units of edge_units", + "default": [ + 0.0, + 0.0, + 0.0 + ], + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + } + ] + }, + "right_edge": { + "title": "Right Edge", + "description": "the right edge (max x, max y, max z) in units of edge_units", + "default": [ + 1.0, + 1.0, + 1.0 + ], + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + } + ] + }, + "resolution": { + "title": "Resolution", + "description": "the resolution at which to sample between the edges.", + "default": [ + 400, + 400, + 400 + ], + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "type": "integer" + }, + { + "type": "integer" + }, + { + "type": "integer" + } + ] + } + } + }, + "DataContainer": { + "title": "DataContainer", + "type": "object", + "properties": { + "filename": { + "title": "Filename", + "description": "the filename for the dataset", + "type": "string" + }, + "selections": { + "title": "Selections", + "description": "list of selections to load in this dataset", + "type": "array", + "items": { + "$ref": "#/definitions/SelectionObject" + } + }, + "edge_units": { + "title": "Edge Units", + "description": "the units to use for left_edge and right_edge in the selections", + "default": "code_length", + "type": "string" + }, + "store_in_cache": { + "title": "Store In Cache", + "description": "if enabled, will store references to yt datasets to avoid reloading.", + "default": true, + "type": "boolean" + } + } + } + } +} From d112971c99c7243fbfd5bb032dcb6abb28b53b7e Mon Sep 17 00:00:00 2001 From: chrishavlin Date: Mon, 3 Apr 2023 17:03:51 -0500 Subject: [PATCH 06/12] run a test on a temporary read-only directory --- src/yt_napari/_tests/test_config.py | 20 ++++++++++++++++++++ src/yt_napari/_tests/test_viewer.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/yt_napari/_tests/test_config.py b/src/yt_napari/_tests/test_config.py index 65c04af..f0fe34c 100644 --- a/src/yt_napari/_tests/test_config.py +++ b/src/yt_napari/_tests/test_config.py @@ -1,4 +1,5 @@ import os +from stat import S_IREAD import pytest @@ -23,3 +24,22 @@ def test_config(tmp_path): with pytest.raises(KeyError, match="bad_key is not a valid option"): custom_config.get_option("bad_key") + + +def test_config_in_read_only(tmp_path, caplog): + config_dir = tmp_path / "configdir" + config_dir.mkdir() + + config_dir.chmod(mode=S_IREAD) + + custom_config = _ConfigContainer(config_dir=str(config_dir)) + custom_config.write_to_disk() + + assert "Could not write" in caplog.text + + custom_config = _ConfigContainer(config_dir=str(config_dir / "another_dir")) + custom_config.write_to_disk() + assert "Could not create" in caplog.text + + config_dir.chmod(0o0777) + config_dir.rmdir() diff --git a/src/yt_napari/_tests/test_viewer.py b/src/yt_napari/_tests/test_viewer.py index cdaffb4..340eb8b 100644 --- a/src/yt_napari/_tests/test_viewer.py +++ b/src/yt_napari/_tests/test_viewer.py @@ -45,7 +45,7 @@ def test_viewer(make_napari_viewer, yt_ds, caplog): # build a new scene so it builds from prior sc = Scene() - sc.add_to_viewer(viewer, yt_ds, ("gas", "density"), resolution=res) + sc.add_to_viewer(viewer, yt_ds, ("gas", "density")) expected_layers += 1 assert len(viewer.layers) == expected_layers From 8e0a77e06579c3aa22ffb8cd0a29f192f58aa58d Mon Sep 17 00:00:00 2001 From: chrishavlin Date: Mon, 3 Apr 2023 17:12:29 -0500 Subject: [PATCH 07/12] skip the new test if on windows --- src/yt_napari/_tests/test_config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/yt_napari/_tests/test_config.py b/src/yt_napari/_tests/test_config.py index f0fe34c..eb28b52 100644 --- a/src/yt_napari/_tests/test_config.py +++ b/src/yt_napari/_tests/test_config.py @@ -1,4 +1,5 @@ import os +import sys from stat import S_IREAD import pytest @@ -26,6 +27,7 @@ def test_config(tmp_path): custom_config.get_option("bad_key") +@pytest.mark.skipif(sys.platform.startswith("win"), reason="Test not valid for windows") def test_config_in_read_only(tmp_path, caplog): config_dir = tmp_path / "configdir" config_dir.mkdir() From ef6b00776913d2762504c1bcbd6cd9e75709dacf Mon Sep 17 00:00:00 2001 From: chrishavlin Date: Mon, 3 Apr 2023 17:44:38 -0500 Subject: [PATCH 08/12] updating docs --- .github/workflows/test_and_deploy.yml | 2 +- docs/quickstart.rst | 27 ++++++++++++++++++++++++ src/yt_napari/config.py | 30 ++++++++++++++++++++++++--- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index 38d2432..bcde39c 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -50,7 +50,7 @@ jobs: # tox-conda: https://github.com/tox-dev/tox-conda - name: Install dependencies run: | - python -m pip install --upgrade "pip < 22" + python -m pip install --upgrade pip python -m pip install setuptools tox tox-gh-actions # this runs the platform-specific tests declared in tox.ini diff --git a/docs/quickstart.rst b/docs/quickstart.rst index a6b834f..18debf0 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -7,6 +7,7 @@ After installation, there are three modes of using :code:`yt-napari`: 2. :ref:`loading a json file from the napari gui` 3. :ref:`napari gui plugins` +Additionally, you can configure some behavior between napari sessions: see :ref:`Configuring yt-napari`. .. _jupyusage: @@ -93,3 +94,29 @@ The use the yt Reader plugin, from a Napari viewer, select "Plugins -> yt-napari .. image:: _static/readme_ex_003_gui_reader.gif The reader plugin does its best to align new selections of data with existing yt-napari image layers and should be able to properly align selections from different yt datasets (please submit a bug report if it fails!). + + +.. _configfile: + +Configuring yt-napari +********************* + +User options are saved between napari sessions using the :code:`yt-napari.toml` configuration file. The location of this file is system-dependent. To find +where it is, from a python shell, the following will display the expected file path for your system: + + +.. code-block:: python + + from yt_napari.config import ytnapari_config + print(ytnapari_config.config_file) + + +you can edit that file directly or use the :code:`ytnapari_config.set_option('name', value)` to change an option and write the option to disk. If the default +directory is not writeable, a warning will be logged but no errors raised. + +Configuration options +##################### + +The following options are available: + +* :code:`in_memory_cache`, :code:`bool` (default :code:`True`). When :code:`True`, the widget and json-readers will store references to yt datasets in an in-memory cache. Subsequents loads of the same dataset will then use the available dataset handle. This behavior can also be manually controlled in the widget and json options -- changing it in the configuration will simply change the default value. diff --git a/src/yt_napari/config.py b/src/yt_napari/config.py index 0888e1e..2916f47 100644 --- a/src/yt_napari/config.py +++ b/src/yt_napari/config.py @@ -9,19 +9,20 @@ _defaults = {"in_memory_cache": True} -class _ConfigContainer: +class ConfigContainer: def __init__(self, config_dir=None): if config_dir is None: app_dirs = platformdirs.AppDirs("yt-napari") self.dir = app_dirs.user_config_dir else: self.dir = config_dir - self.config_file_name = "yt-napari.yaml" + self.config_file_name = "yt-napari.toml" self.config_file = os.path.join(self.dir, self.config_file_name) self.config_dict = {} self.load_config() def load_config(self): + """(re)loads the configuration file from disk""" if os.path.isfile(self.config_file): with open(self.config_file, "rb") as fi: config_dict = tomli.load(fi) @@ -32,6 +33,17 @@ def load_config(self): self.config_dict.update(config_dict) def set_option(self, option: str, value): + """ + set a configuraiton option (and write to disk if possible) + + Parameters + ---------- + option : str + the name of the option + value + the value of the option + + """ if option not in _defaults: raise KeyError(f"{option} is not a valid option") @@ -39,6 +51,9 @@ def set_option(self, option: str, value): self.write_to_disk() def write_to_disk(self): + """ + attempts to write the configuration to disk. + """ if os.path.exists(self.dir) is False: try: os.makedirs(self.dir, exist_ok=True) @@ -52,10 +67,19 @@ def write_to_disk(self): ytnapari_log.warning(f"Could not write {self.config_file}") def get_option(self, option: str): + """ + get the value of a config option + + Parameters + ---------- + option: str + the option to return + + """ if option not in self.config_dict: raise KeyError(f"{option} is not a valid option") return self.config_dict[option] -ytnapari_config = _ConfigContainer() +ytnapari_config = ConfigContainer() From f69953cbd8adebf4265a12c3d84eee443bf7d3f0 Mon Sep 17 00:00:00 2001 From: Chris Havlin Date: Tue, 4 Apr 2023 10:12:28 -0500 Subject: [PATCH 09/12] use hard references --- src/yt_napari/_ds_cache.py | 17 ++--------------- src/yt_napari/_tests/test_config.py | 10 +++++----- src/yt_napari/_tests/test_ds_cache.py | 10 +++------- 3 files changed, 10 insertions(+), 27 deletions(-) diff --git a/src/yt_napari/_ds_cache.py b/src/yt_napari/_ds_cache.py index 2f1eb9e..c371852 100644 --- a/src/yt_napari/_ds_cache.py +++ b/src/yt_napari/_ds_cache.py @@ -1,5 +1,3 @@ -import weakref - import yt from yt_napari.config import ytnapari_config @@ -14,7 +12,7 @@ def __init__(self): def add_ds(self, ds, name: str): if name in self.available: ytnapari_log.warning(f"A dataset already exists for {name}. Overwriting.") - self.available[name] = weakref.proxy(ds) + self.available[name] = ds self._most_recent = name @property @@ -32,17 +30,6 @@ def get_ds(self, name: str): def exists(self, name: str) -> bool: return name in self.available - def reference_exists(self, name: str) -> bool: - if self.exists(name): - ds = self.get_ds(name) - ref_exists = True - try: - _ = ds.basename - except ReferenceError: - ref_exists = False - return ref_exists - return False - def rm_ds(self, name: str): self.available.pop(name, None) @@ -51,7 +38,7 @@ def rm_all(self): self._most_recent = None def check_then_load(self, filename: str): - if self.reference_exists(filename): + if self.exists(filename): ytnapari_log.info(f"loading {filename} from cache.") return self.get_ds(filename) else: diff --git a/src/yt_napari/_tests/test_config.py b/src/yt_napari/_tests/test_config.py index eb28b52..ed06234 100644 --- a/src/yt_napari/_tests/test_config.py +++ b/src/yt_napari/_tests/test_config.py @@ -4,20 +4,20 @@ import pytest -from yt_napari.config import _ConfigContainer +from yt_napari.config import ConfigContainer def test_config(tmp_path): config_dir = str(tmp_path / "configdir") - custom_config = _ConfigContainer(config_dir=config_dir) + custom_config = ConfigContainer(config_dir=config_dir) custom_config.set_option("in_memory_cache", False) # default is True config_file = custom_config.config_file assert os.path.isfile(config_file) # load as new config object to make sure it updates from the on-disk config - custom_config = _ConfigContainer(config_dir=config_dir) + custom_config = ConfigContainer(config_dir=config_dir) assert custom_config.get_option("in_memory_cache") is False with pytest.raises(KeyError, match="bad_key is not a valid option"): @@ -34,12 +34,12 @@ def test_config_in_read_only(tmp_path, caplog): config_dir.chmod(mode=S_IREAD) - custom_config = _ConfigContainer(config_dir=str(config_dir)) + custom_config = ConfigContainer(config_dir=str(config_dir)) custom_config.write_to_disk() assert "Could not write" in caplog.text - custom_config = _ConfigContainer(config_dir=str(config_dir / "another_dir")) + custom_config = ConfigContainer(config_dir=str(config_dir / "another_dir")) custom_config.write_to_disk() assert "Could not create" in caplog.text diff --git a/src/yt_napari/_tests/test_ds_cache.py b/src/yt_napari/_tests/test_ds_cache.py index a758657..efe3711 100644 --- a/src/yt_napari/_tests/test_ds_cache.py +++ b/src/yt_napari/_tests/test_ds_cache.py @@ -39,15 +39,11 @@ def test_ds_cache(caplog): assert dataset_cache.most_recent is None -def _add_to_cache_then_delete(): +def test_ds_destruction(): ds = get_new_ds() dataset_cache.add_ds(ds, "hellotest") - del ds - - -def test_weakref_destruction(): - _add_to_cache_then_delete() - assert dataset_cache.reference_exists("hellotest") is False + dataset_cache.rm_ds("hellotest") + assert dataset_cache.exists("hellotest") is False def test_config_option(yt_ugrid_ds_fn): From fe514af12894c91c510679444cb3dc51614f766d Mon Sep 17 00:00:00 2001 From: Chris Havlin Date: Tue, 4 Apr 2023 10:29:23 -0500 Subject: [PATCH 10/12] add a clear cache button to widget --- src/yt_napari/_tests/test_widget_reader.py | 4 ++++ src/yt_napari/_widget_reader.py | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/src/yt_napari/_tests/test_widget_reader.py b/src/yt_napari/_tests/test_widget_reader.py index af6386f..1888348 100644 --- a/src/yt_napari/_tests/test_widget_reader.py +++ b/src/yt_napari/_tests/test_widget_reader.py @@ -2,6 +2,7 @@ import numpy as np +from yt_napari._ds_cache import dataset_cache from yt_napari._widget_reader import ReaderWidget @@ -44,3 +45,6 @@ def rebuild_data(final_shape, data): temp_layer = viewer.layers[1] assert temp_layer.metadata["_yt_napari_layer"] is True + + r.clear_cache() + assert len(dataset_cache.available) == 0 diff --git a/src/yt_napari/_widget_reader.py b/src/yt_napari/_widget_reader.py index 738783d..9907fcf 100644 --- a/src/yt_napari/_widget_reader.py +++ b/src/yt_napari/_widget_reader.py @@ -5,6 +5,7 @@ from qtpy.QtWidgets import QVBoxLayout, QWidget from yt_napari import _data_model, _gui_utilities, _model_ingestor +from yt_napari._ds_cache import dataset_cache from yt_napari.viewer import Scene @@ -22,6 +23,9 @@ def __init__(self, napari_viewer: "napari.viewer.Viewer", parent=None): pb = widgets.PushButton(text="Load") pb.clicked.connect(self.load_data) self.big_container.append(pb) + cc = widgets.PushButton(text="Clear cache") + cc.clicked.connect(self.clear_cache) + self.big_container.append(cc) self.layout().addWidget(self.big_container.native) _yt_scene: Scene = None # will persist across widget calls @@ -32,6 +36,9 @@ def yt_scene(self): self._yt_scene = Scene() return self._yt_scene + def clear_cache(self): + dataset_cache.rm_all() + def load_data(self): # first extract all the pydantic arguments from the container py_kwargs = {} From c6116d804e2d3480bc4715a331870b46a299d586 Mon Sep 17 00:00:00 2001 From: chrishavlin Date: Thu, 6 Apr 2023 17:36:39 -0500 Subject: [PATCH 11/12] use the yt config --- src/yt_napari/_data_model.py | 4 +- src/yt_napari/_ds_cache.py | 4 +- src/yt_napari/_tests/test_config.py | 57 +++++------------ src/yt_napari/_tests/test_ds_cache.py | 6 +- src/yt_napari/config.py | 91 ++++----------------------- 5 files changed, 34 insertions(+), 128 deletions(-) diff --git a/src/yt_napari/_data_model.py b/src/yt_napari/_data_model.py index d08e5c9..c5f77ac 100644 --- a/src/yt_napari/_data_model.py +++ b/src/yt_napari/_data_model.py @@ -4,7 +4,7 @@ from pydantic import BaseModel, Field -from yt_napari.config import ytnapari_config +from yt_napari.config import ytcfg from yt_napari.schemas import _manager @@ -44,7 +44,7 @@ class DataContainer(BaseModel): description="the units to use for left_edge and right_edge in the selections", ) store_in_cache: Optional[bool] = Field( - ytnapari_config.get_option("in_memory_cache"), + ytcfg.get("yt_napari", "in_memory_cache"), description="if enabled, will store references to yt datasets.", ) diff --git a/src/yt_napari/_ds_cache.py b/src/yt_napari/_ds_cache.py index 2f1eb9e..dc1d30c 100644 --- a/src/yt_napari/_ds_cache.py +++ b/src/yt_napari/_ds_cache.py @@ -2,7 +2,7 @@ import yt -from yt_napari.config import ytnapari_config +from yt_napari.config import ytcfg from yt_napari.logging import ytnapari_log @@ -56,7 +56,7 @@ def check_then_load(self, filename: str): return self.get_ds(filename) else: ds = yt.load(filename) - if ytnapari_config.get_option("in_memory_cache"): + if ytcfg.get("yt_napari", "in_memory_cache"): self.add_ds(ds, filename) return ds diff --git a/src/yt_napari/_tests/test_config.py b/src/yt_napari/_tests/test_config.py index eb28b52..9948b68 100644 --- a/src/yt_napari/_tests/test_config.py +++ b/src/yt_napari/_tests/test_config.py @@ -1,47 +1,18 @@ -import os -import sys -from stat import S_IREAD +def test_config_update(): -import pytest + # note: when the following import is at the top of the file, it errors: + # UnboundLocalError: local variable 'ytcfg' referenced before assignment + from yt_napari.config import _defaults, _get_updated_config, ytcfg -from yt_napari.config import _ConfigContainer + current_vals = {} + for setting in _defaults.keys(): + current_vals[setting] = ytcfg.get("yt_napari", setting) + ytcfg.remove_section("yt_napari") + ytcfg = _get_updated_config(ytcfg) + assert ytcfg.has_section("yt_napari") + for setting, val in _defaults.items(): + assert val == ytcfg.get("yt_napari", setting) -def test_config(tmp_path): - - config_dir = str(tmp_path / "configdir") - custom_config = _ConfigContainer(config_dir=config_dir) - custom_config.set_option("in_memory_cache", False) # default is True - - config_file = custom_config.config_file - assert os.path.isfile(config_file) - - # load as new config object to make sure it updates from the on-disk config - custom_config = _ConfigContainer(config_dir=config_dir) - assert custom_config.get_option("in_memory_cache") is False - - with pytest.raises(KeyError, match="bad_key is not a valid option"): - custom_config.set_option("bad_key", False) - - with pytest.raises(KeyError, match="bad_key is not a valid option"): - custom_config.get_option("bad_key") - - -@pytest.mark.skipif(sys.platform.startswith("win"), reason="Test not valid for windows") -def test_config_in_read_only(tmp_path, caplog): - config_dir = tmp_path / "configdir" - config_dir.mkdir() - - config_dir.chmod(mode=S_IREAD) - - custom_config = _ConfigContainer(config_dir=str(config_dir)) - custom_config.write_to_disk() - - assert "Could not write" in caplog.text - - custom_config = _ConfigContainer(config_dir=str(config_dir / "another_dir")) - custom_config.write_to_disk() - assert "Could not create" in caplog.text - - config_dir.chmod(0o0777) - config_dir.rmdir() + # make sure our settings get reset to what they were coming in... + ytcfg.update({"yt_napari": current_vals}) diff --git a/src/yt_napari/_tests/test_ds_cache.py b/src/yt_napari/_tests/test_ds_cache.py index a758657..0da69ea 100644 --- a/src/yt_napari/_tests/test_ds_cache.py +++ b/src/yt_napari/_tests/test_ds_cache.py @@ -1,7 +1,7 @@ from yt import testing as yt_testing from yt_napari._ds_cache import dataset_cache -from yt_napari.config import ytnapari_config +from yt_napari.config import ytcfg def get_new_ds(): @@ -52,7 +52,7 @@ def test_weakref_destruction(): def test_config_option(yt_ugrid_ds_fn): dataset_cache.rm_all() - ytnapari_config.set_option("in_memory_cache", False) + ytcfg.set("yt_napari", "in_memory_cache", False) _ = dataset_cache.check_then_load(yt_ugrid_ds_fn) assert yt_ugrid_ds_fn not in dataset_cache.available - ytnapari_config.set_option("in_memory_cache", True) + ytcfg.set("yt_napari", "in_memory_cache", True) diff --git a/src/yt_napari/config.py b/src/yt_napari/config.py index 2916f47..f9ddaaa 100644 --- a/src/yt_napari/config.py +++ b/src/yt_napari/config.py @@ -1,85 +1,20 @@ -import os - -import platformdirs -import tomli -import tomli_w - -from yt_napari.logging import ytnapari_log +from yt.config import ytcfg _defaults = {"in_memory_cache": True} -class ConfigContainer: - def __init__(self, config_dir=None): - if config_dir is None: - app_dirs = platformdirs.AppDirs("yt-napari") - self.dir = app_dirs.user_config_dir - else: - self.dir = config_dir - self.config_file_name = "yt-napari.toml" - self.config_file = os.path.join(self.dir, self.config_file_name) - self.config_dict = {} - self.load_config() - - def load_config(self): - """(re)loads the configuration file from disk""" - if os.path.isfile(self.config_file): - with open(self.config_file, "rb") as fi: - config_dict = tomli.load(fi) - else: - config_dict = {} - - self.config_dict = _defaults.copy() - self.config_dict.update(config_dict) - - def set_option(self, option: str, value): - """ - set a configuraiton option (and write to disk if possible) - - Parameters - ---------- - option : str - the name of the option - value - the value of the option - - """ - if option not in _defaults: - raise KeyError(f"{option} is not a valid option") - - self.config_dict[option] = value - self.write_to_disk() - - def write_to_disk(self): - """ - attempts to write the configuration to disk. - """ - if os.path.exists(self.dir) is False: +def _get_updated_config(cfg): + # adds the yt_napari section and missing settings to the base yt config + if cfg.has_section("yt_napari"): + for setting, default in _defaults.items(): try: - os.makedirs(self.dir, exist_ok=True) - except OSError: - ytnapari_log.warning(f"Could not create {self.dir}") - - try: - with open(self.config_file, "wb") as fi: - tomli_w.dump(self.config_dict, fi) - except OSError: - ytnapari_log.warning(f"Could not write {self.config_file}") - - def get_option(self, option: str): - """ - get the value of a config option - - Parameters - ---------- - option: str - the option to return - - """ - if option not in self.config_dict: - raise KeyError(f"{option} is not a valid option") - - return self.config_dict[option] + _ = cfg.get("yt_napari", setting) + except KeyError: + cfg.set("yt_napari", setting, default) + else: + cfg.add_section("yt_napari") + cfg.update({"yt_napari": _defaults}) + return cfg -ytnapari_config = ConfigContainer() +ytcfg = _get_updated_config(ytcfg) From fa35b707057824bfdd21b11e2bc881c46e7f3d09 Mon Sep 17 00:00:00 2001 From: Chris Havlin Date: Fri, 7 Apr 2023 10:54:20 -0500 Subject: [PATCH 12/12] more tests, update docs on config, rm now unnecessary deps --- docs/quickstart.rst | 30 ++++++++++++++++++++--------- setup.cfg | 2 -- src/yt_napari/_tests/test_config.py | 12 ++++++++++++ 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 18debf0..8f75273 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -101,22 +101,34 @@ The reader plugin does its best to align new selections of data with existing yt Configuring yt-napari ********************* -User options are saved between napari sessions using the :code:`yt-napari.toml` configuration file. The location of this file is system-dependent. To find -where it is, from a python shell, the following will display the expected file path for your system: +User options can be saved between napari sessions by adding to the base :code:`yt` configuration +file, :code:`yt.toml`. :code:`yt` looks for the configuration file in a number of places (check +out the :code:`yt` documentation +on:ref:`configuration `_ ). To add +:code:`yt-napari` options, open up (or create) the configuration file and add a +:code:`[yt_napari]` section. An example configuration file might look like: +.. code-block:: bash -.. code-block:: python - - from yt_napari.config import ytnapari_config - print(ytnapari_config.config_file) + [yt] + log_level = 1 + test_data_dir = "/path/to/yt_data" + [yt_napari] + in_memory_cache = true -you can edit that file directly or use the :code:`ytnapari_config.set_option('name', value)` to change an option and write the option to disk. If the default -directory is not writeable, a warning will be logged but no errors raised. Configuration options ##################### The following options are available: -* :code:`in_memory_cache`, :code:`bool` (default :code:`True`). When :code:`True`, the widget and json-readers will store references to yt datasets in an in-memory cache. Subsequents loads of the same dataset will then use the available dataset handle. This behavior can also be manually controlled in the widget and json options -- changing it in the configuration will simply change the default value. +* :code:`in_memory_cache`, :code:`bool` (default :code:`true`). When :code:`true`, +the widget and json-readers will store references to yt datasets in an in-memory +cache. Subsequents loads of the same dataset will then use the available dataset +handle. This behavior can also be manually controlled in the widget and json +options -- changing it in the configuration will simply change the default value. + + +Note that boolean values in :code:`toml` files start with lowercase: :code:`true` and +:code:`false` (instead of :code:`True` and :code:`False`). diff --git a/setup.cfg b/setup.cfg index a4ac876..9313df7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,8 +36,6 @@ install_requires = npe2 numpy pydantic - tomli - tomli-w yt>=4.0.1 python_requires = >=3.8 package_dir = diff --git a/src/yt_napari/_tests/test_config.py b/src/yt_napari/_tests/test_config.py index 9948b68..9c72ec9 100644 --- a/src/yt_napari/_tests/test_config.py +++ b/src/yt_napari/_tests/test_config.py @@ -14,5 +14,17 @@ def test_config_update(): for setting, val in _defaults.items(): assert val == ytcfg.get("yt_napari", setting) + # run it through again, make sure existing values are preserved + ytcfg.set("yt_napari", "in_memory_cache", False) # (default is True) + ytcfg = _get_updated_config(ytcfg) + assert ytcfg.get("yt_napari", "in_memory_cache") is False + + # remove it and add a blank so that the defaults get applied + ytcfg.remove_section("yt_napari") + ytcfg.add_section("yt_napari") + ytcfg = _get_updated_config(ytcfg) + for setting, val in _defaults.items(): + assert val == ytcfg.get("yt_napari", setting) + # make sure our settings get reset to what they were coming in... ytcfg.update({"yt_napari": current_vals})