Skip to content

Commit

Permalink
Merge pull request #44 from chrishavlin/metadata_widget
Browse files Browse the repository at this point in the history
Metadata widget
  • Loading branch information
chrishavlin authored Sep 1, 2023
2 parents fc43a2d + e505c5d commit ba69b8f
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 3 deletions.
11 changes: 11 additions & 0 deletions src/yt_napari/_data_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,14 @@ def _store_schema(schema_db: Optional[Union[PosixPath, str]] = None, **kwargs):
m = _manager.Manager(schema_db)
prefix, schema_contents = _get_standard_schema_contents()
m.write_new_schema(schema_contents, schema_prefix=prefix, **kwargs)


class MetadataModel(BaseModel):
filename: str = Field(None, description="the filename for the dataset")
include_field_list: bool = Field(True, description="whether to list the fields")
_ds_attrs: Tuple[str] = (
"domain_left_edge",
"domain_right_edge",
"current_time",
"domain_dimensions",
)
19 changes: 16 additions & 3 deletions src/yt_napari/_ds_cache.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import os.path
from os import PathLike
from typing import Optional

import yt

from yt_napari import _special_loaders
Expand Down Expand Up @@ -42,11 +46,11 @@ def check_then_load(self, filename: str, cache_if_not_found: bool = True):
if self.exists(filename):
ytnapari_log.info(f"loading {filename} from cache.")
return self.get_ds(filename)
elif filename.startswith("_ytnapari") and hasattr(_special_loaders, filename):
elif callable_name := _check_for_special(filename):
# the filename is actually a function handle! get it, call it
# this allows yt-napari to to use all the yt fake datasets in
# this allows yt-napari to use all the yt fake datasets in
# testing without saving them to disk.
ds_callable = getattr(_special_loaders, filename)
ds_callable = getattr(_special_loaders, callable_name)
ds = ds_callable()
else:
ds = yt.load(filename)
Expand All @@ -57,3 +61,12 @@ def check_then_load(self, filename: str, cache_if_not_found: bool = True):


dataset_cache = DatasetCache()


def _check_for_special(filename: PathLike) -> Optional[str]:
# check if a "filename" is one of our short-circuiting special loaders
# and return the function name if it is valid.
basename = os.path.basename(filename)
if basename.startswith("_ytnapari") and hasattr(_special_loaders, basename):
return str(basename)
return None
13 changes: 13 additions & 0 deletions src/yt_napari/_gui_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,13 @@ def _register_yt_data_model(translator: MagicPydanticRegistry):
magicgui_args=(py_model.__fields__[field]),
pydantic_attr_factory=embed_in_list,
)
translator.register(
_data_model.MetadataModel,
"filename",
magicgui_factory=get_file_widget,
magicgui_kwargs={"name": "filename"},
pydantic_attr_factory=get_filename,
)

translator.register(
_data_model.TimeSeriesFileSelection,
Expand Down Expand Up @@ -306,3 +313,9 @@ def get_yt_selection_container(selection_type: str, return_native: bool = False)
if return_native:
return selection_container.native
return selection_container


def get_yt_metadata_container():
data_container = widgets.Container()
translator.add_pydantic_to_container(_data_model.MetadataModel, data_container)
return data_container
17 changes: 17 additions & 0 deletions src/yt_napari/_model_ingestor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from yt_napari._data_model import (
DataContainer,
InputModel,
MetadataModel,
Region,
SelectionObject,
Slice,
Expand Down Expand Up @@ -793,3 +794,19 @@ def _load_with_timeseries_specials_check(file):
else:
ds = yt.load(file)
return ds


def _process_metadata_model(model: MetadataModel) -> Tuple[dict, dict]:
fname = model.filename
ds = dataset_cache.check_then_load(fname)
meta_data_dict = {}
for attr in model._ds_attrs:
meta_data_dict[attr] = getattr(ds, attr)

fields_by_type = defaultdict(lambda: [])
if model.include_field_list:
fields = ds.field_list
for field_type, field in fields:
fields_by_type[field_type].append(field)

return meta_data_dict, fields_by_type
25 changes: 25 additions & 0 deletions src/yt_napari/_tests/test_metadata_widget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from yt_napari._widget_matadata import LayersList, MetadataWidget


def test_widget_reader(make_napari_viewer):
viewer = make_napari_viewer()
r = MetadataWidget(napari_viewer=viewer)
r.metadata_input_container.filename.value = "_ytnapari_load_grid"
r.inspect_file()
r.inspect_file() # do it again to hit that clear statement

assert "stream" in r.field_lists.keys()

r.field_lists["stream"].expand()
r.field_lists["stream"].expand()

assert "domain_left_edge" in r.array_vals.keys()
r.array_vals["domain_left_edge"].update_units("km")
r.deleteLater()


def test_layer_list():
ll = LayersList("test_layer", range(0, 10), expand=False)
ll.expand()
assert ll.currently_expanded is True
ll.deleteLater()
152 changes: 152 additions & 0 deletions src/yt_napari/_widget_matadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
from typing import Callable, Optional

import napari
from magicgui import widgets
from qtpy import QtCore
from qtpy.QtGui import QStandardItem, QStandardItemModel
from qtpy.QtWidgets import (
QAbstractItemView,
QComboBox,
QHBoxLayout,
QLabel,
QListView,
QPushButton,
QVBoxLayout,
QWidget,
)
from unyt import unyt_array

from yt_napari import _data_model, _gui_utilities, _model_ingestor


class MetadataWidget(QWidget):
def __init__(self, napari_viewer: "napari.viewer.Viewer", parent=None):
super().__init__(parent)
self.setLayout(QVBoxLayout())
self.viewer = napari_viewer

self.big_container = widgets.Container()
self.metadata_input_container = _gui_utilities.get_yt_metadata_container()
self.big_container.append(self.metadata_input_container)
self._post_load_function: Optional[Callable] = None

pb = widgets.PushButton(text="Inspect")
pb.clicked.connect(self.inspect_file)
self.big_container.append(pb)

self.meta_data_display = widgets.Container()
self.big_container.append(self.meta_data_display)
self.layout().addWidget(self.big_container.native)
self.widgets_to_clear: list = None

def inspect_file(self):
if self.widgets_to_clear is not None:
# always clear out the widgets when the button is pushed
for list_widget in self.widgets_to_clear:
self.layout().removeWidget(list_widget)
list_widget.setParent(None)
self.widgets_to_clear = []
self.field_lists = {}
self.array_vals = {}
py_kwargs = {}

_gui_utilities.translator.get_pydantic_kwargs(
self.metadata_input_container, _data_model.MetadataModel, py_kwargs
)

# instantiate the base model
model = _data_model.MetadataModel.parse_obj(py_kwargs)

# process it!
meta_data_dict, fields_by_type = _model_ingestor._process_metadata_model(model)

# display the metadata
for attr, val in meta_data_dict.items():
if isinstance(val, unyt_array):
newid = UnytArrayQWidget(attr, val)
self.array_vals[attr] = newid
else:
newid = QLabel(f"{attr}: {str(val)}")
self.widgets_to_clear.append(newid)
self.layout().addWidget(newid)

# the collapsible field display
ilist = 0
for ftype, fields in fields_by_type.items():
new_field_list = LayersList(ftype, fields, ilist < 3)
ilist += 1
self.field_lists[ftype] = new_field_list
self.widgets_to_clear.append(new_field_list)
self.layout().addWidget(new_field_list)


# based on answer here:
# https://stackoverflow.com/questions/11077793/is-there-a-standard-component-for-collapsible-panel-in-qt


class LayersList(QWidget):
"""
LayerList class which acts as collapsable list.
"""

def __init__(self, name, layers, expand=True):
super().__init__()
self.currently_expanded = True
self.main_layout = QVBoxLayout()
self.main_layout.setSpacing(0)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.expand_button = QPushButton(name)
self.expand_button.setToolTip(f"List of {name} Layers")
self.layer_list = QListView()
self.layer_list.setDragEnabled(True)
self.container_model = QStandardItemModel()
for layer in layers:
layer_label = QStandardItem(layer)
self.container_model.appendRow(layer_label)
self.layer_list.setModel(self.container_model)
self.layer_list.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.main_layout.addWidget(self.expand_button)
self.main_layout.addWidget(self.layer_list)
self.expand_button.clicked.connect(self.expand)
self.setLayout(self.main_layout)
self.resized_size = int(16 * len(layers))
if not expand:
self.expand()

@QtCore.Slot()
def expand(self):
if self.currently_expanded:
self.layer_list.setMaximumHeight(0)
self.currently_expanded = False
else:
self.layer_list.setMaximumHeight(self.resized_size)
self.currently_expanded = True


class UnytArrayQWidget(QWidget):
# based of of yt.units.display_ytarray() : simpler to rewrite it than try
# to convert the ipywidget to Qt.
def __init__(self, arr_name: str, arr: unyt_array):
super().__init__()

unit_registry = arr.units.registry
self._unit_options = unit_registry.list_same_dimensions(arr.units)
self.arr = arr.copy()
self.arr_name = arr_name
self.units_box = QComboBox()
self.units_box.addItems(self._unit_options)
self.units_box.setCurrentText(str(self.arr.units))
self.units_box.currentTextChanged.connect(self.update_units)
self.arr_display = QLabel(self._get_display_name_text())

self.main_layout = QHBoxLayout()
self.main_layout.addWidget(self.arr_display)
self.main_layout.addWidget(self.units_box)
self.setLayout(self.main_layout)

def update_units(self, new_units_str):
self.arr = self.arr.to(new_units_str)
self.arr_display.setText(self._get_display_name_text())

def _get_display_name_text(self):
return f"{self.arr_name}: {str(self.arr.value)}"
5 changes: 5 additions & 0 deletions src/yt_napari/napari.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ contributions:
- id: yt-napari.timeseries_widget
title: Read 2D selections from yt timeseries
python_name: yt_napari._widget_reader:TimeSeriesReader
- id: yt-napari.metadata_widget
title: Inspect the metadata for a yt dataset
python_name: yt_napari._widget_matadata:MetadataWidget
readers:
- command: yt-napari.get_reader
accepts_directories: false
Expand All @@ -20,3 +23,5 @@ contributions:
display_name: yt Reader
- command: yt-napari.timeseries_widget
display_name: yt Time Series Reader
- command: yt-napari.metadata_widget
display_name: yt Metadata Explorer

0 comments on commit ba69b8f

Please sign in to comment.