From 9173429eee74a0cc5a7709f8d6c700169675b55d Mon Sep 17 00:00:00 2001 From: Andy Sweet Date: Wed, 7 Jun 2023 18:35:15 -0700 Subject: [PATCH 01/17] Use client instead of s3fs --- setup.cfg | 1 + src/napari_cryoet_data_portal/__init__.py | 4 +- .../_listing_widget.py | 43 ++++----- .../_metadata_widget.py | 18 +--- src/napari_cryoet_data_portal/_model.py | 95 ------------------- src/napari_cryoet_data_portal/_open_widget.py | 38 ++++---- src/napari_cryoet_data_portal/_reader.py | 69 ++++++-------- .../_tests/test_model.py | 25 ----- .../_tests/test_reader.py | 15 +-- src/napari_cryoet_data_portal/_uri_widget.py | 32 +++---- src/napari_cryoet_data_portal/_widget.py | 14 ++- src/napari_cryoet_data_portal/napari.yaml | 2 +- 12 files changed, 105 insertions(+), 251 deletions(-) delete mode 100644 src/napari_cryoet_data_portal/_model.py delete mode 100644 src/napari_cryoet_data_portal/_tests/test_model.py diff --git a/setup.cfg b/setup.cfg index 101d7c6..a8c568f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,6 +31,7 @@ project_urls = [options] packages = find: install_requires = + cryoet_data_portal npe2 numpy napari_ome_zarr diff --git a/src/napari_cryoet_data_portal/__init__.py b/src/napari_cryoet_data_portal/__init__.py index 76abd68..4a98fff 100644 --- a/src/napari_cryoet_data_portal/__init__.py +++ b/src/napari_cryoet_data_portal/__init__.py @@ -5,7 +5,7 @@ from ._logging import logger from ._reader import ( points_annotations_reader, - read_points_annotations_json, + read_points_annotations_ndjson, read_tomogram_ome_zarr, tomogram_ome_zarr_reader, ) @@ -16,6 +16,6 @@ "logger", "points_annotations_reader", "read_tomogram_ome_zarr", - "read_points_annotations_json", + "read_points_annotations_ndjson", "tomogram_ome_zarr_reader", ) diff --git a/src/napari_cryoet_data_portal/_listing_widget.py b/src/napari_cryoet_data_portal/_listing_widget.py index cb218c8..b2d6578 100644 --- a/src/napari_cryoet_data_portal/_listing_widget.py +++ b/src/napari_cryoet_data_portal/_listing_widget.py @@ -1,4 +1,4 @@ -from typing import Generator, Optional +from typing import Generator, List, Optional, Tuple from qtpy.QtCore import Qt from qtpy.QtWidgets import ( @@ -8,11 +8,10 @@ QVBoxLayout, QWidget, ) +from cryoet_data_portal import Client, Dataset, Tomogram -from napari_cryoet_data_portal._io import list_dir from napari_cryoet_data_portal._listing_tree_widget import ListingTreeWidget from napari_cryoet_data_portal._logging import logger -from napari_cryoet_data_portal._model import Dataset from napari_cryoet_data_portal._progress_widget import ProgressWidget from napari_cryoet_data_portal._vendored.superqt._searchable_tree_widget import ( _update_visible_items, @@ -42,35 +41,33 @@ def __init__(self, parent: Optional[QWidget] = None) -> None: layout.addStretch(0) self.setLayout(layout) - def load(self, path: str) -> None: - logger.debug("ListingWidget.load: %s", path) + def load(self, client: Client) -> None: + logger.debug("ListingWidget.load: %s", client) self.tree.clear() self.show() - self._progress.submit(path) + self._progress.submit(client) def cancel(self) -> None: logger.debug("ListingWidget.cancel") self._progress.cancel() - def _loadDatasets(self, path: str) -> Generator[Dataset, None, None]: - logger.debug("ListingWidget._loadDatasets: %s", path) - # TODO: only list non-hidden directories. - dataset_names = tuple( - p for p in list_dir(path) if not p.startswith(".") - ) - if len(dataset_names) == 0: - logger.debug("No datasets found") - for name in dataset_names: - yield Dataset.from_data_path_and_name(path, name) + def _loadDatasets(self, client: str) -> Generator[Tuple[Dataset, List[Tomogram]], None, None]: + logger.debug("ListingWidget._loadDatasets: %s", client) + for dataset in Dataset.find(client): + tomograms: List[Tomogram] = [] + for run in dataset.runs: + tomograms.extend(run.tomograms) + yield dataset, tomograms - def _onDatasetLoaded(self, dataset: Dataset) -> None: - logger.debug("ListingWidget._onDatasetLoaded: %s", dataset.name) - text = f"{dataset.name} ({len(dataset.subjects)})" + def _onDatasetLoaded(self, result: Tuple[Dataset, List[Tomogram]]) -> None: + dataset, tomograms = result + logger.debug("ListingWidget._onDatasetLoaded: %s", dataset.id) + text = f"{dataset.id} ({len(tomograms)})" item = QTreeWidgetItem((text,)) item.setData(0, Qt.ItemDataRole.UserRole, dataset) - for s in dataset.subjects: - subject_item = QTreeWidgetItem((s.name,)) - subject_item.setData(0, Qt.ItemDataRole.UserRole, s) - item.addChild(subject_item) + for tomogram in tomograms: + tomogram_item = QTreeWidgetItem((str(tomogram.id),)) + tomogram_item.setData(0, Qt.ItemDataRole.UserRole, tomogram) + item.addChild(tomogram_item) _update_visible_items(item, self.tree.last_filter) self.tree.addTopLevelItem(item) diff --git a/src/napari_cryoet_data_portal/_metadata_widget.py b/src/napari_cryoet_data_portal/_metadata_widget.py index 212ae9f..7da52b7 100644 --- a/src/napari_cryoet_data_portal/_metadata_widget.py +++ b/src/napari_cryoet_data_portal/_metadata_widget.py @@ -5,10 +5,9 @@ QVBoxLayout, QWidget, ) +from cryoet_data_portal import Dataset, Tomogram -from napari_cryoet_data_portal._io import read_json from napari_cryoet_data_portal._logging import logger -from napari_cryoet_data_portal._model import Dataset, Subject from napari_cryoet_data_portal._progress_widget import ProgressWidget from napari_cryoet_data_portal._vendored.superqt._searchable_tree_widget import ( QSearchableTreeWidget, @@ -33,10 +32,10 @@ def __init__(self, parent: Optional[QWidget] = None) -> None: layout.addStretch(0) self.setLayout(layout) - def load(self, data: Union[Dataset, Subject]) -> None: + def load(self, data: Union[Dataset, Tomogram]) -> None: logger.debug("MetadataWidget.load: %s", data) self._main.tree.clear() - self.setTitle(f"Metadata: {data.name}") + self.setTitle(f"Metadata: {data.id}") self.show() self._progress.submit(data) @@ -44,16 +43,9 @@ def cancel(self) -> None: logger.debug("MetadataWidget.cancel") self._progress.cancel() - def _loadMetadata(self, data: Union[Dataset, Subject]) -> Dict[str, Any]: + def _loadMetadata(self, data: Union[Dataset, Tomogram]) -> Dict[str, Any]: logger.debug("MetadataWidget._loadMetadata: %s", data) - path = "" - if isinstance(data, Dataset): - path = data.metadata_path - elif isinstance(data, Subject): - path = data.tomogram_metadata_path - else: - raise AssertionError("Expected Dataset or Subject data") - return read_json(path) + return data.to_dict() def _onMetadataLoaded(self, metadata: Dict[str, Any]) -> None: logger.debug("MetadataWidget._onMetadataLoaded: %s", metadata) diff --git a/src/napari_cryoet_data_portal/_model.py b/src/napari_cryoet_data_portal/_model.py deleted file mode 100644 index 63c3146..0000000 --- a/src/napari_cryoet_data_portal/_model.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Data model used to abstract the contents and structure of the portal.""" - -from dataclasses import dataclass, field -from functools import cached_property -from typing import Tuple - -from napari_cryoet_data_portal._io import list_dir - -PORTAL_S3_URI = "s3://cryoet-data-portal-public" - - -@dataclass(frozen=True) -class Subject: - """Represents a subject or tomogram within a dataset. - - Attributes - ---------- - name : str - The name of the subject (e.g. 'TS_026'). - path : str - The full directory-like path associated with the subject - (e.g. 's3://cryoet-data-portal-public/10000/TS_026'). - image_path : str - The full directory-like path to the tomogram as an OME-Zarr multi-scale image - (e.g. 's3://cryoet-data-portal-public/10000/TS_026/Tomograms/CanonicalTomogram/TS_026.zarr'). - annotation_paths : tuple of str - The full file-like paths to the annotation JSON files. - (e.g. ['s3://cryoet-data-portal-public/10000/TS_026/Tomograms/Annotations/julia_mahamid-ribosome-1.0.json', ...]). - """ - - name: str - path: str - image_path: str - annotation_paths: Tuple[str, ...] - - @cached_property - def tomogram_metadata_path(self) -> str: - return ( - f"{self.path}/Tomograms/CanonicalTomogram/tomogram_metadata.json" - ) - - @classmethod - def from_dataset_path_and_name( - cls, dataset_path: str, name: str - ) -> "Subject": - path = f"{dataset_path}/{name}" - layer_path = f"{path}/Tomograms" - image_path = f"{layer_path}/CanonicalTomogram/{name}.zarr" - annotation_dir = f"{layer_path}/Annotations" - annotation_paths = tuple( - f"{annotation_dir}/{p}" - for p in list_dir(annotation_dir) - if p.endswith(".json") - ) - return cls( - name=name, - path=path, - image_path=image_path, - annotation_paths=annotation_paths, - ) - - -@dataclass(frozen=True) -class Dataset: - """Represents an entire dataset of many subjects and their annotations. - - Attributes - ---------- - name : str - The name of the dataset (e.g. '10000'). - path : str - The full directory-like path associated with the dataset - (e.g. 's3://cryoet-data-portal-public/10000'). - subjects : tuple of subjects - The subjects within the dataset. - """ - - name: str - path: str - subjects: Tuple[Subject, ...] = field(repr=False) - - @cached_property - def metadata_path(self) -> str: - return f"{self.path}/dataset_metadata.json" - - @classmethod - def from_data_path_and_name(cls, data_path: str, name: str) -> "Dataset": - path = f"{data_path}/{name}" - subjects = tuple( - Subject.from_dataset_path_and_name(path, p) - for p in list_dir(path) - # TODO: better way to select non-hidden directories. - if "." not in p - ) - return Dataset(name=name, path=path, subjects=subjects) diff --git a/src/napari_cryoet_data_portal/_open_widget.py b/src/napari_cryoet_data_portal/_open_widget.py index f3951f6..b8aef88 100644 --- a/src/napari_cryoet_data_portal/_open_widget.py +++ b/src/napari_cryoet_data_portal/_open_widget.py @@ -13,12 +13,12 @@ QVBoxLayout, QWidget, ) +from cryoet_data_portal import Tomogram from napari_cryoet_data_portal._logging import logger -from napari_cryoet_data_portal._model import Subject from napari_cryoet_data_portal._progress_widget import ProgressWidget from napari_cryoet_data_portal._reader import ( - read_points_annotations_json, + read_annotation_points, read_tomogram_ome_zarr, ) @@ -54,7 +54,7 @@ def __init__( super().__init__(parent) self._viewer = viewer - self._subject: Optional[Subject] = None + self._tomogram: Optional[Tomogram] = None self.setTitle("Tomogram") @@ -69,7 +69,7 @@ def __init__( self.resolution.setCurrentText(LOW_RESOLUTION.name) self.resolution_label.setBuddy(self.resolution) self._progress = ProgressWidget( - work=self._loadSubject, + work=self._loadTomogram, yieldCallback=self._onLayerLoaded, ) @@ -86,35 +86,34 @@ def __init__( layout.addWidget(self._progress) self.setLayout(layout) - def setSubject(self, subject: Subject) -> None: + def setTomogram(self, tomogram: Tomogram) -> None: self.cancel() - self._subject = subject + self._tomogram = tomogram # Reset resolution to low to handle case when user tries # out a higher resolution but then moves onto another tomogram. self.resolution.setCurrentText(LOW_RESOLUTION.name) - self.setTitle(f"Tomogram: {subject.name}") + self.setTitle(f"Tomogram: {tomogram.id}") self.show() self.load() def load(self) -> None: resolution = self.resolution.currentData() - logger.debug("OpenWidget.load: %s", self._subject, resolution) + logger.debug("OpenWidget.load: %s", self._tomogram, resolution) self._viewer.layers.clear() - self._progress.submit(self._subject, resolution) + self._progress.submit(self._tomogram, resolution) def cancel(self) -> None: logger.debug("OpenWidget.cancel") self._progress.cancel() - def _loadSubject( + def _loadTomogram( self, - subject: Subject, + tomogram: Tomogram, resolution: Resolution, ) -> Generator[FullLayerData, None, None]: - logger.debug("OpenWidget._loadSubject: %s", subject.name) - image_data, image_attrs, _ = read_tomogram_ome_zarr(subject.image_path) - # TODO: read JSON metadata in reader to get name from there. - image_attrs["name"] = f"{subject.name}-tomogram" + logger.debug("OpenWidget._loadTomogram: %s", tomogram.id) + image_data, image_attrs, _ = read_tomogram_ome_zarr(tomogram.https_omezarr_dir) + image_attrs["name"] = f"{tomogram.name}-tomogram" # Skip indexing for multi-resolution to avoid adding any # unnecessary nodes to the dask compute graph. if resolution is not MULTI_RESOLUTION: @@ -127,13 +126,8 @@ def _loadSubject( ) yield image_data, image_attrs, "image" - for p in subject.annotation_paths: - points_data, points_attrs, _ = read_points_annotations_json(p) - annotation_name = points_attrs["name"] - points_attrs["name"] = f"{subject.name}-{annotation_name}" - yield points_data, points_attrs, "points" - - return subject + for annotation in tomogram.run.annotations: + yield read_annotation_points(annotation) def _onLayerLoaded(self, layer_data: FullLayerData) -> None: logger.debug("OpenWidget._onLayerLoaded") diff --git a/src/napari_cryoet_data_portal/_reader.py b/src/napari_cryoet_data_portal/_reader.py index 388e368..fc87e94 100644 --- a/src/napari_cryoet_data_portal/_reader.py +++ b/src/napari_cryoet_data_portal/_reader.py @@ -1,14 +1,12 @@ """Functions to read data from the portal into napari types.""" -import json -import os from typing import Any, Dict, List, Optional, Tuple +import fsspec import ndjson from napari_ome_zarr import napari_get_reader from npe2.types import FullLayerData, PathOrPaths, ReaderFunction - -from napari_cryoet_data_portal._io import get_open, read_json, s3_to_https +from cryoet_data_portal import Annotation def tomogram_ome_zarr_reader(path: PathOrPaths) -> Optional[ReaderFunction]: @@ -21,10 +19,6 @@ def _read_many_tomograms_ome_zarr(paths: PathOrPaths) -> List[FullLayerData]: return [read_tomogram_ome_zarr(p) for p in paths] -def read_tomogram_metadata(path: str) -> Dict[str, Any]: - return read_json(path) - - def read_tomogram_ome_zarr(path: str) -> FullLayerData: """Reads a napari image layer from a tomogram in the OME-Zarr format. @@ -45,7 +39,6 @@ def read_tomogram_ome_zarr(path: str) -> FullLayerData: >>> data, attrs, _ = read_tomogram_ome_zarr('s3://cryoet-data-portal-public/10000/TS_026/Tomograms/CanonicalTomogram/TS_026.zarr') >>> image = Image(data, **attrs) """ - path = s3_to_https(path) reader = napari_get_reader(path) layers = reader(path) return layers[0] @@ -57,7 +50,7 @@ def points_annotations_reader(path: PathOrPaths) -> Optional[ReaderFunction]: Parameters ---------- path : str or sequence of str - The path or paths of the annotation JSON file(s) to read. + The path or paths of the annotation NDJSON file(s) to read. Returns ------- @@ -70,26 +63,37 @@ def points_annotations_reader(path: PathOrPaths) -> Optional[ReaderFunction]: Examples -------- - >>> path = ['julia_mahamid-ribosome-1.0.json', 'julia_mahamid-fatty_acid_synthase-1.0.json'] + >>> path = ['julia_mahamid-ribosome-1.0.ndjson', 'julia_mahamid-fatty_acid_synthase-1.0.ndjson'] >>> reader = points_annotations_reader(path) >>> data, attrs, _ = reader(path) """ - return _read_many_points_annotations + return _read_many_points_annotations_ndjson -def _read_many_points_annotations(paths: PathOrPaths) -> List[FullLayerData]: +def _read_many_points_annotations_ndjson(paths: PathOrPaths) -> List[FullLayerData]: if isinstance(paths, str): paths = [paths] - return [read_points_annotations_json(p) for p in paths] + return [read_points_annotations_ndjson(p) for p in paths] + + +def read_points_annotations_ndjson(path: str) -> FullLayerData: + data = _read_points_data(path) + attributes = { + "name": "annotations", + "size": 14, + "face_color": "red", + "opacity": 0.5, + } + return data, attributes, "points" -def read_points_annotations_json(path: str) -> FullLayerData: - """Reads a napari points layer from one annotation JSON File. +def read_annotation_points(annotation: Annotation) -> FullLayerData: + """Reads a napari points layer from an annotation. Parameters ---------- - path : str - The path of the annotation JSON file to read. + annotation : Annotation + The tomogram annotation. Returns ------- @@ -100,35 +104,20 @@ def read_points_annotations_json(path: str) -> FullLayerData: Examples -------- >>> from napari.layers import Points - >>> data, attrs, _ = read_points_annotations_json('julia_mahamid-ribosome-1.0.json') + >>> data, attrs, _ = read_annotation_points('julia_mahamid-ribosome-1.0.json') >>> points = Points(data, **attrs) """ - data: List[Tuple[float, float, float]] = [] - open_ = get_open(path) - with open_(path) as f: - metadata = json.load(f) - data_dir = os.path.dirname(path) - for sub_file in metadata["files"]: - sub_name = sub_file["file"] - sub_path = f"{data_dir}/{sub_name}" - sub_data = _read_points_annotations_ndjson(sub_path) - data.extend(sub_data) - attributes = { - "name": metadata["annotation_object"]["name"], - "metadata": metadata, - "size": 14, - "face_color": "red", - "opacity": 0.5, - } - return data, attributes, "points" + data, attributes, layer_type = read_points_annotations_ndjson(annotation.https_annotations_path) + attributes["name"] = annotation.id + attributes["metadata"] = annotation.to_dict() + return data, attributes, layer_type -def _read_points_annotations_ndjson( +def _read_points_data( path: str, ) -> List[Tuple[float, float, float]]: data: List[Tuple[float, float, float]] = [] - open_ = get_open(path) - with open_(path) as f: + with fsspec.open(path) as f: sub_data = [ _annotation_to_point(annotation) for annotation in ndjson.load(f) diff --git a/src/napari_cryoet_data_portal/_tests/test_model.py b/src/napari_cryoet_data_portal/_tests/test_model.py deleted file mode 100644 index b47c8fb..0000000 --- a/src/napari_cryoet_data_portal/_tests/test_model.py +++ /dev/null @@ -1,25 +0,0 @@ -from napari_cryoet_data_portal._model import PORTAL_S3_URI, Dataset, Subject - - -def test_dataset_from_data_path_and_name(): - dataset = Dataset.from_data_path_and_name(PORTAL_S3_URI, "10000") - - assert dataset.name == "10000" - assert dataset.path == f"{PORTAL_S3_URI}/10000" - assert len(dataset.subjects) > 0 - subject_names = tuple(s.name for s in dataset.subjects) - assert "TS_026" in subject_names - - -def test_subject_from_dataset_path_and_name(): - dataset_dir = f"{PORTAL_S3_URI}/10000" - - subject = Subject.from_dataset_path_and_name(dataset_dir, "TS_026") - - assert subject.name == "TS_026" - assert subject.path == f"{dataset_dir}/TS_026" - assert ( - subject.image_path - == f"{dataset_dir}/TS_026/Tomograms/CanonicalTomogram/TS_026.zarr" - ) - assert len(subject.annotation_paths) > 0 diff --git a/src/napari_cryoet_data_portal/_tests/test_reader.py b/src/napari_cryoet_data_portal/_tests/test_reader.py index 52978a9..2b935ca 100644 --- a/src/napari_cryoet_data_portal/_tests/test_reader.py +++ b/src/napari_cryoet_data_portal/_tests/test_reader.py @@ -2,15 +2,16 @@ from napari import Viewer from napari.layers import Points +from cryoet_data_portal import Annotation, Client from napari_cryoet_data_portal import ( - read_points_annotations_json, + read_points_annotations_ndjson, read_tomogram_ome_zarr, ) -from napari_cryoet_data_portal._model import PORTAL_S3_URI -TOMOGRAM_DIR = f"{PORTAL_S3_URI}/10000/TS_026/Tomograms" -ANNOTATION_FILE = f"{TOMOGRAM_DIR}/Annotations/julia_mahamid-ribosome-1.0.json" +CLOUDFRONT_URI = "https://files.cryoetdataportal.cziscience.com" +TOMOGRAM_DIR = f"{CLOUDFRONT_URI}/10000/TS_026/Tomograms" +ANNOTATION_FILE = f"{TOMOGRAM_DIR}/Annotations/julia_mahamid-ribosome-1.0.ndjson" def test_read_tomogram_ome_zarr(): @@ -26,14 +27,14 @@ def test_read_tomogram_ome_zarr(): assert layer_type == "image" -def test_read_points_annotations_json(): - data, attrs, layer_type = read_points_annotations_json(ANNOTATION_FILE) +def test_read_points_annotations_ndjson(): + data, attrs, layer_type = read_points_annotations_ndjson(ANNOTATION_FILE) assert len(data) == 838 assert data[0] == (469, 261, 517) assert data[418] == (524, 831, 475) assert data[837] == (519, 723, 538) - assert attrs["name"] == "Ribosome" + assert attrs["name"] == "annotations" assert layer_type == "points" diff --git a/src/napari_cryoet_data_portal/_uri_widget.py b/src/napari_cryoet_data_portal/_uri_widget.py index 99549a5..115bb24 100644 --- a/src/napari_cryoet_data_portal/_uri_widget.py +++ b/src/napari_cryoet_data_portal/_uri_widget.py @@ -11,15 +11,16 @@ QVBoxLayout, QWidget, ) +from cryoet_data_portal import Client -from napari_cryoet_data_portal._io import path_exists from napari_cryoet_data_portal._logging import logger -from napari_cryoet_data_portal._model import PORTAL_S3_URI from napari_cryoet_data_portal._progress_widget import ProgressWidget +GRAPHQL_URI = "https://graphql.cryoetdataportal.cziscience.com/v1/graphql" + class UriWidget(QGroupBox): - connected = Signal(str) + connected = Signal(object) disconnected = Signal() def __init__(self, parent: Optional[QWidget] = None) -> None: @@ -28,7 +29,7 @@ def __init__(self, parent: Optional[QWidget] = None) -> None: self.setTitle("Portal") self._connect_button = QPushButton("Connect") self._disconnect_button = QPushButton("Disconnect") - self._uri_edit = QLineEdit(PORTAL_S3_URI) + self._uri_edit = QLineEdit(GRAPHQL_URI) self._uri_edit.setCursorPosition(0) self._uri_edit.setPlaceholderText("Enter a URI to CryoET portal data") choose_dir_icon = self.style().standardIcon( @@ -36,8 +37,8 @@ def __init__(self, parent: Optional[QWidget] = None) -> None: ) self._choose_dir_button = QPushButton(choose_dir_icon, "") self._progress = ProgressWidget( - work=self._checkUri, - returnCallback=self._onUriChecked, + work=self._connect, + returnCallback=self._onConnected, ) self._updateVisibility(False) @@ -70,18 +71,13 @@ def _onDisconnectClicked(self) -> None: self._updateVisibility(False) self.disconnected.emit() - def _checkUri(self, uri: str) -> Tuple[bool, str]: - logger.debug("UriWidget._checkUri: %s", uri) - return path_exists(uri), uri + def _connect(self, uri: str) -> Client: + return Client(uri) - def _onUriChecked(self, result: Tuple[bool, str]) -> None: - logger.debug("UriWidget._onUriChecked: %s", result) - exists, uri = result - self._updateVisibility(exists) - if exists: - self.connected.emit(uri) - else: - raise ValueError(f"CryoET data portal not found at: {uri}") + def _onConnected(self, client: Client) -> None: + logger.debug("UriWidget._onConnected: %s", client) + self._updateVisibility(True) + self.connected.emit(client) def _updateVisibility(self, uri_exists: bool) -> None: logger.debug("UriWidget._updateVisibility: %s", uri_exists) @@ -91,6 +87,6 @@ def _updateVisibility(self, uri_exists: bool) -> None: self._uri_edit.setReadOnly(uri_exists) def _onChooseDirClicked(self) -> None: - logger.debug("DataPathWidget._onChooseDirClicked") + logger.debug("UriWidget._onChooseDirClicked") path = QFileDialog.getExistingDirectory(self) self._uri_edit.setText(path) diff --git a/src/napari_cryoet_data_portal/_widget.py b/src/napari_cryoet_data_portal/_widget.py index 898f43c..5d230ab 100644 --- a/src/napari_cryoet_data_portal/_widget.py +++ b/src/napari_cryoet_data_portal/_widget.py @@ -6,11 +6,11 @@ QVBoxLayout, QWidget, ) +from cryoet_data_portal import Client, Tomogram from napari_cryoet_data_portal._listing_widget import ListingWidget from napari_cryoet_data_portal._logging import logger from napari_cryoet_data_portal._metadata_widget import MetadataWidget -from napari_cryoet_data_portal._model import Subject from napari_cryoet_data_portal._open_widget import OpenWidget from napari_cryoet_data_portal._uri_widget import UriWidget @@ -26,6 +26,8 @@ def __init__( ) -> None: super().__init__(parent) + self._client: Optional[Client] = None + self._uri = UriWidget() self._listing = ListingWidget() @@ -52,12 +54,14 @@ def __init__( self.setLayout(layout) - def _onUriConnected(self, uri: str) -> None: + def _onUriConnected(self, client: Client) -> None: logger.debug("DataPortalWidget._onUriConnected") - self._listing.load(uri) + self._client = client + self._listing.load(client) def _onUriDisconnected(self) -> None: logger.debug("DataPortalWidget._onUriDisconnected") + self._client = None for widget in (self._listing, self._metadata, self._open): widget.cancel() widget.hide() @@ -68,7 +72,7 @@ def _onListingItemChanged( logger.debug("DataPortalWidget._onListingItemClicked: %s", item) data = item.data(0, Qt.ItemDataRole.UserRole) self._metadata.load(data) - if isinstance(data, Subject): - self._open.setSubject(data) + if isinstance(data, Tomogram): + self._open.setTomogram(data) else: self._open.hide() diff --git a/src/napari_cryoet_data_portal/napari.yaml b/src/napari_cryoet_data_portal/napari.yaml index 128f9f1..4586b12 100644 --- a/src/napari_cryoet_data_portal/napari.yaml +++ b/src/napari_cryoet_data_portal/napari.yaml @@ -24,7 +24,7 @@ contributions: accepts_directories: true - command: napari-cryoet-data-portal.points_annotations_reader filename_patterns: - - '*.json' + - '*.ndjson' accepts_directories: false sample_data: - key: tomogram-10000-ts-026 From 755b8ec02e0649184ff14dd9187682feec030939 Mon Sep 17 00:00:00 2001 From: Andy Sweet Date: Wed, 7 Jun 2023 18:42:25 -0700 Subject: [PATCH 02/17] Use tomogram name instead of id --- src/napari_cryoet_data_portal/_listing_widget.py | 2 +- src/napari_cryoet_data_portal/_open_widget.py | 4 ++-- src/napari_cryoet_data_portal/_uri_widget.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/napari_cryoet_data_portal/_listing_widget.py b/src/napari_cryoet_data_portal/_listing_widget.py index b2d6578..3e54598 100644 --- a/src/napari_cryoet_data_portal/_listing_widget.py +++ b/src/napari_cryoet_data_portal/_listing_widget.py @@ -66,7 +66,7 @@ def _onDatasetLoaded(self, result: Tuple[Dataset, List[Tomogram]]) -> None: item = QTreeWidgetItem((text,)) item.setData(0, Qt.ItemDataRole.UserRole, dataset) for tomogram in tomograms: - tomogram_item = QTreeWidgetItem((str(tomogram.id),)) + tomogram_item = QTreeWidgetItem((tomogram.name,)) tomogram_item.setData(0, Qt.ItemDataRole.UserRole, tomogram) item.addChild(tomogram_item) _update_visible_items(item, self.tree.last_filter) diff --git a/src/napari_cryoet_data_portal/_open_widget.py b/src/napari_cryoet_data_portal/_open_widget.py index b8aef88..1a9b3b8 100644 --- a/src/napari_cryoet_data_portal/_open_widget.py +++ b/src/napari_cryoet_data_portal/_open_widget.py @@ -92,7 +92,7 @@ def setTomogram(self, tomogram: Tomogram) -> None: # Reset resolution to low to handle case when user tries # out a higher resolution but then moves onto another tomogram. self.resolution.setCurrentText(LOW_RESOLUTION.name) - self.setTitle(f"Tomogram: {tomogram.id}") + self.setTitle(f"Tomogram: {tomogram.name}") self.show() self.load() @@ -111,7 +111,7 @@ def _loadTomogram( tomogram: Tomogram, resolution: Resolution, ) -> Generator[FullLayerData, None, None]: - logger.debug("OpenWidget._loadTomogram: %s", tomogram.id) + logger.debug("OpenWidget._loadTomogram: %s", tomogram.name) image_data, image_attrs, _ = read_tomogram_ome_zarr(tomogram.https_omezarr_dir) image_attrs["name"] = f"{tomogram.name}-tomogram" # Skip indexing for multi-resolution to avoid adding any diff --git a/src/napari_cryoet_data_portal/_uri_widget.py b/src/napari_cryoet_data_portal/_uri_widget.py index 115bb24..2c8a4ff 100644 --- a/src/napari_cryoet_data_portal/_uri_widget.py +++ b/src/napari_cryoet_data_portal/_uri_widget.py @@ -1,4 +1,4 @@ -from typing import Optional, Tuple +from typing import Optional from qtpy.QtCore import Signal from qtpy.QtWidgets import ( From 90c5399baf696e48157153747e4a5f46071038f6 Mon Sep 17 00:00:00 2001 From: Andy Sweet Date: Wed, 7 Jun 2023 18:43:44 -0700 Subject: [PATCH 03/17] Add fsspec as explicit dependency --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index a8c568f..6cf9e63 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,6 +32,7 @@ project_urls = packages = find: install_requires = cryoet_data_portal + fsspec npe2 numpy napari_ome_zarr From ea5afd935eabf02be1d4c36d720839674d39938d Mon Sep 17 00:00:00 2001 From: Andy Sweet Date: Wed, 7 Jun 2023 18:57:43 -0700 Subject: [PATCH 04/17] Fix fsspec dependency --- setup.cfg | 2 +- src/napari_cryoet_data_portal/_open_widget.py | 5 ++++- src/napari_cryoet_data_portal/_reader.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 6cf9e63..226b856 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,7 +32,7 @@ project_urls = packages = find: install_requires = cryoet_data_portal - fsspec + fsspec[https] npe2 numpy napari_ome_zarr diff --git a/src/napari_cryoet_data_portal/_open_widget.py b/src/napari_cryoet_data_portal/_open_widget.py index 1a9b3b8..f00fe6f 100644 --- a/src/napari_cryoet_data_portal/_open_widget.py +++ b/src/napari_cryoet_data_portal/_open_widget.py @@ -127,7 +127,10 @@ def _loadTomogram( yield image_data, image_attrs, "image" for annotation in tomogram.run.annotations: - yield read_annotation_points(annotation) + data, attrs, layer_type = read_annotation_points(annotation) + name = attrs["name"] + attrs["name"] = f"{tomogram.name}-{name}" + yield data, attrs, layer_type def _onLayerLoaded(self, layer_data: FullLayerData) -> None: logger.debug("OpenWidget._onLayerLoaded") diff --git a/src/napari_cryoet_data_portal/_reader.py b/src/napari_cryoet_data_portal/_reader.py index fc87e94..bcf19c6 100644 --- a/src/napari_cryoet_data_portal/_reader.py +++ b/src/napari_cryoet_data_portal/_reader.py @@ -108,7 +108,7 @@ def read_annotation_points(annotation: Annotation) -> FullLayerData: >>> points = Points(data, **attrs) """ data, attributes, layer_type = read_points_annotations_ndjson(annotation.https_annotations_path) - attributes["name"] = annotation.id + attributes["name"] = annotation.object_name attributes["metadata"] = annotation.to_dict() return data, attributes, layer_type From 6c7f3c5e5de2392a34ddd349d20c0a9b03124489 Mon Sep 17 00:00:00 2001 From: Andy Sweet Date: Thu, 8 Jun 2023 09:46:46 -0700 Subject: [PATCH 05/17] New client for each data query --- src/napari_cryoet_data_portal/_listing_widget.py | 3 ++- src/napari_cryoet_data_portal/_open_widget.py | 11 +++++++++-- src/napari_cryoet_data_portal/_tests/test_reader.py | 4 ++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/napari_cryoet_data_portal/_listing_widget.py b/src/napari_cryoet_data_portal/_listing_widget.py index 3e54598..3ecaac8 100644 --- a/src/napari_cryoet_data_portal/_listing_widget.py +++ b/src/napari_cryoet_data_portal/_listing_widget.py @@ -56,7 +56,8 @@ def _loadDatasets(self, client: str) -> Generator[Tuple[Dataset, List[Tomogram]] for dataset in Dataset.find(client): tomograms: List[Tomogram] = [] for run in dataset.runs: - tomograms.extend(run.tomograms) + for spacing in run.tomogram_voxel_spacings: + tomograms.extend(spacing.tomograms) yield dataset, tomograms def _onDatasetLoaded(self, result: Tuple[Dataset, List[Tomogram]]) -> None: diff --git a/src/napari_cryoet_data_portal/_open_widget.py b/src/napari_cryoet_data_portal/_open_widget.py index f00fe6f..1debd59 100644 --- a/src/napari_cryoet_data_portal/_open_widget.py +++ b/src/napari_cryoet_data_portal/_open_widget.py @@ -13,7 +13,7 @@ QVBoxLayout, QWidget, ) -from cryoet_data_portal import Tomogram +from cryoet_data_portal import Annotation, Client, Tomogram from napari_cryoet_data_portal._logging import logger from napari_cryoet_data_portal._progress_widget import ProgressWidget @@ -126,7 +126,14 @@ def _loadTomogram( ) yield image_data, image_attrs, "image" - for annotation in tomogram.run.annotations: + # Looking up tomogram.tomogram_voxel_spacing.annotations triggers a query + # using the client from where the tomogram was found. + # A single client is not thread safe, so we need a new instance for each query. + # TODO: pass through URI in setTomogram from main widget. + client = Client() + annotations = Annotation.find(client, [Annotation.tomogram_voxel_spacing_id == tomogram.tomogram_voxel_spacing_id]) + + for annotation in annotations: data, attrs, layer_type = read_annotation_points(annotation) name = attrs["name"] attrs["name"] = f"{tomogram.name}-{name}" diff --git a/src/napari_cryoet_data_portal/_tests/test_reader.py b/src/napari_cryoet_data_portal/_tests/test_reader.py index 2b935ca..dbfaf1b 100644 --- a/src/napari_cryoet_data_portal/_tests/test_reader.py +++ b/src/napari_cryoet_data_portal/_tests/test_reader.py @@ -10,8 +10,8 @@ ) CLOUDFRONT_URI = "https://files.cryoetdataportal.cziscience.com" -TOMOGRAM_DIR = f"{CLOUDFRONT_URI}/10000/TS_026/Tomograms" -ANNOTATION_FILE = f"{TOMOGRAM_DIR}/Annotations/julia_mahamid-ribosome-1.0.ndjson" +TOMOGRAM_DIR = f"{CLOUDFRONT_URI}/10000/TS_026/Tomograms/VoxelSpacing13.48" +ANNOTATION_FILE = f"{TOMOGRAM_DIR}/Annotations/sara_goetz-ribosome-1.0.ndjson" def test_read_tomogram_ome_zarr(): From 3ab860dea479f42eaff86d2de2128c717dab51e2 Mon Sep 17 00:00:00 2001 From: Andy Sweet Date: Fri, 30 Jun 2023 15:38:47 -0700 Subject: [PATCH 06/17] Fix tests --- setup.cfg | 1 - src/napari_cryoet_data_portal/__init__.py | 3 +- src/napari_cryoet_data_portal/_io.py | 56 ------ .../_metadata_widget.py | 1 - src/napari_cryoet_data_portal/_reader.py | 2 - src/napari_cryoet_data_portal/_sample_data.py | 25 +-- .../_tests/_mocks.py | 168 ------------------ .../_tests/conftest.py | 18 ++ .../_tests/test_listing_widget.py | 31 +--- .../_tests/test_metadata_widget.py | 42 +---- .../_tests/test_model.py | 25 --- .../_tests/test_open_widget.py | 19 +- .../_tests/test_uri_widget.py | 45 +---- .../_tests/test_widget.py | 8 +- src/napari_cryoet_data_portal/_uri_widget.py | 16 +- 15 files changed, 71 insertions(+), 389 deletions(-) delete mode 100644 src/napari_cryoet_data_portal/_io.py delete mode 100644 src/napari_cryoet_data_portal/_tests/_mocks.py create mode 100644 src/napari_cryoet_data_portal/_tests/conftest.py delete mode 100644 src/napari_cryoet_data_portal/_tests/test_model.py diff --git a/setup.cfg b/setup.cfg index a2b2baf..8e2f492 100644 --- a/setup.cfg +++ b/setup.cfg @@ -59,7 +59,6 @@ testing = tox pytest pytest-cov - pytest-mock pytest-qt napari pyqt5 diff --git a/src/napari_cryoet_data_portal/__init__.py b/src/napari_cryoet_data_portal/__init__.py index e002fda..8fb3e24 100644 --- a/src/napari_cryoet_data_portal/__init__.py +++ b/src/napari_cryoet_data_portal/__init__.py @@ -2,9 +2,9 @@ from ._version import version as __version__ except ImportError: __version__ = "unknown" -from ._logging import logger from ._reader import ( points_annotations_reader, + read_annotation_points, read_points_annotations_ndjson, read_tomogram_ome_zarr, tomogram_ome_zarr_reader, @@ -14,6 +14,7 @@ __all__ = ( "DataPortalWidget", "points_annotations_reader", + "read_annotation_points", "read_tomogram_ome_zarr", "read_points_annotations_ndjson", "tomogram_ome_zarr_reader", diff --git a/src/napari_cryoet_data_portal/_io.py b/src/napari_cryoet_data_portal/_io.py deleted file mode 100644 index c119c9b..0000000 --- a/src/napari_cryoet_data_portal/_io.py +++ /dev/null @@ -1,56 +0,0 @@ -"""IO utilities for abstracting paths across S3, HTTPS, and a local filesystem.""" - -import json -import os -from typing import Any, Callable, Dict, List, Tuple - -from s3fs import S3FileSystem - - -def list_dir(path: str) -> Tuple[str, ...]: - """Returns the names of the contents of a directory-like path.""" - names = _list_dir_s3(path) if _is_s3(path) else os.listdir(path) - return tuple(sorted(names)) - - -def path_exists(path: str) -> bool: - """Returns true if the given path exists, false otherwise.""" - return _path_exists_s3(path) if _is_s3(path) else os.path.exists(path) - - -def get_open(path: str) -> Callable[[str], Any]: - """Returns a function to open a file at the given path.""" - if _is_s3(path): - s3 = S3FileSystem(anon=True) - return s3.open - return open - - -def s3_to_https(path: str) -> str: - """Converts an S3 URI to the equivalent CloudFront HTTPS URI.""" - return path.replace( - "s3://cryoet-data-portal-public", - "https://files.cryoetdataportal.cziscience.com", - ) - - -def read_json(path: str) -> Dict[str, Any]: - """Reads JSON from the given path.""" - open_ = get_open(path) - with open_(path) as f: - return json.load(f) - - -def _is_s3(path: str) -> bool: - return path.startswith("s3://") - - -def _path_exists_s3(path: str) -> bool: - s3 = S3FileSystem(anon=True) - return s3.exists(path) - - -def _list_dir_s3(path: str) -> List[str]: - s3 = S3FileSystem(anon=True) - bucket_path = path.replace("s3://", "") - return [p.split("/")[-1] for p in s3.ls(bucket_path)] diff --git a/src/napari_cryoet_data_portal/_metadata_widget.py b/src/napari_cryoet_data_portal/_metadata_widget.py index 5329ca9..58d5896 100644 --- a/src/napari_cryoet_data_portal/_metadata_widget.py +++ b/src/napari_cryoet_data_portal/_metadata_widget.py @@ -8,7 +8,6 @@ from cryoet_data_portal import Dataset, Tomogram from napari_cryoet_data_portal._logging import logger -from napari_cryoet_data_portal._model import Dataset, Tomogram from napari_cryoet_data_portal._progress_widget import ProgressWidget from napari_cryoet_data_portal._vendored.superqt._searchable_tree_widget import ( QSearchableTreeWidget, diff --git a/src/napari_cryoet_data_portal/_reader.py b/src/napari_cryoet_data_portal/_reader.py index f4ce2ad..3843433 100644 --- a/src/napari_cryoet_data_portal/_reader.py +++ b/src/napari_cryoet_data_portal/_reader.py @@ -8,8 +8,6 @@ from npe2.types import FullLayerData, PathOrPaths, ReaderFunction from cryoet_data_portal import Annotation -from napari_cryoet_data_portal._io import get_open, s3_to_https - OBJECT_COLOR = { 'ribosome': 'red', diff --git a/src/napari_cryoet_data_portal/_sample_data.py b/src/napari_cryoet_data_portal/_sample_data.py index 58d2968..a8979e7 100644 --- a/src/napari_cryoet_data_portal/_sample_data.py +++ b/src/napari_cryoet_data_portal/_sample_data.py @@ -2,9 +2,10 @@ import numpy as np from npe2.types import FullLayerData +from cryoet_data_portal import Client, Tomogram, TomogramVoxelSpacing from napari_cryoet_data_portal import ( - read_points_annotations_json, + read_annotation_points, read_tomogram_ome_zarr, ) @@ -20,24 +21,24 @@ def tomogram_10000_ts_027() -> List[FullLayerData]: def _read_tomogram_from_10000(name: str) -> List[FullLayerData]: - base_uri = f"s3://cryoet-data-portal-public/10000/{name}/Tomograms/VoxelSpacing13.48" - tomogram_uri = f"{base_uri}/CanonicalTomogram/{name}.zarr" - annotations_uri = f"{base_uri}/Annotations" - ribosome_uri = f"{annotations_uri}/sara_goetz-ribosome-1.0.json" - fatty_acid_uri = ( - f"{annotations_uri}/sara_goetz-fatty_acid_synthase-1.0.json" - ) - - tomogram_image = read_tomogram_ome_zarr(tomogram_uri) + client = Client() + + tomogram_spacing_url = f"https://files.cryoetdataportal.cziscience.com/10000/{name}/Tomograms/VoxelSpacing13.48/" + tomogram_spacing = next(TomogramVoxelSpacing.find(client, [TomogramVoxelSpacing.https_prefix == tomogram_spacing_url])) + + tomogram: Tomogram = next(tomogram_spacing.tomograms) + + tomogram_image = read_tomogram_ome_zarr(tomogram.https_omezarr_dir) # Materialize lowest resolution for speed. tomogram_image = (np.asarray(tomogram_image[0][-1]), *tomogram_image[1:]) tomogram_image[1]["scale"] = (4, 4, 4) # TODO: fix this in reader or data. tomogram_image[1]["name"] = "Tomogram" - ribosome_points = read_points_annotations_json(ribosome_uri) + annotations = tuple(tomogram_spacing.annotations) + ribosome_points = read_annotation_points(annotations[0]) - fatty_acid_points = read_points_annotations_json(fatty_acid_uri) + fatty_acid_points = read_annotation_points(annotations[1]) # Make different annotations distinctive. fatty_acid_points[1]["face_color"] = "blue" diff --git a/src/napari_cryoet_data_portal/_tests/_mocks.py b/src/napari_cryoet_data_portal/_tests/_mocks.py deleted file mode 100644 index 53e74c8..0000000 --- a/src/napari_cryoet_data_portal/_tests/_mocks.py +++ /dev/null @@ -1,168 +0,0 @@ -from typing import Any, Dict, Tuple - -import numpy as np - -from napari_cryoet_data_portal._model import Dataset, Tomogram - - -MOCK_S3_URI = 's3://mock-portal' - - -def mock_dataset( - *, - name: str, - tomograms: Tuple[Tomogram, ...], -) -> Dataset: - path = f"{MOCK_S3_URI}/{name}" - return Dataset( - name=name, - path=path, - tomograms=tomograms, - ) - - -def mock_tomogram( - *, - dataset_name: str, - tomogram_name: str, - voxel_spacing: str, - annotation_names: Tuple[str, ...] -) -> Tomogram: - path = f"{MOCK_S3_URI}/{dataset_name}/{tomogram_name}" - tomogram_path = f"{path}/Tomograms/VoxelSpacing{voxel_spacing}" - image_path = f"{tomogram_path}/CanonicalTomogram/{tomogram_name}.zarr" - annotations_path = f"{tomogram_path}/Annotations" - annotations_paths = tuple( - f"{annotations_path}/{name}.json" - for name in annotation_names - ) - return Tomogram( - name=tomogram_name, - path=path, - tomogram_path=tomogram_path, - image_path=image_path, - annotation_paths=annotations_paths, - ) - - -MOCK_TOMOGRAM_TS_026 = mock_tomogram( - dataset_name="10000", - tomogram_name="TS_026", - voxel_spacing="13.48", - annotation_names=("ribosome", "fatty-acid-synthase"), -) - -MOCK_TOMOGRAM_TS_026_METADATA = { - 'run_name': 'TS_026', - 'voxel_spacing': 13.48, - 'size': { - 'z': 8, - 'y': 8, - 'x': 8, - } -} - -MOCK_TOMOGRAM_TS_026_IMAGE_DATA = [ - np.zeros((8, 8, 8)), - np.zeros((4, 4, 4)), - np.zeros((2, 2, 2)), -] - -MOCK_TOMOGRAM_TS_026_IMAGE_ATTRS = { - 'name': 'TS_026', - 'scale': (1, 1, 1), -} - -MOCK_TOMOGRAM_TS_026_RIBOSOME_DATA = [[0, 0, 0], [2, 2, 2]] -MOCK_TOMOGRAM_TS_026_RIBOSOME_ATTRS = {'name': 'ribosome'} - -MOCK_TOMOGRAM_TS_026_FAS_DATA = [[1, 1, 1], [3, 3, 3]] -MOCK_TOMOGRAM_TS_026_FAS_ATTRS = {'name': 'fatty acid synthase'} - -MOCK_TOMOGRAM_TS_027 = mock_tomogram( - dataset_name="10000", - tomogram_name="TS_027", - voxel_spacing="13.48", - annotation_names=("ribosome", "fatty-acid-synthase"), -) - -MOCK_DATASET_10000 = mock_dataset( - name="10000", - tomograms=(MOCK_TOMOGRAM_TS_026, MOCK_TOMOGRAM_TS_027), -) - -MOCK_DATASET_10000_METADATA = { - 'dataset_title': 'mock dataset', - 'authors': [ - { - 'name': 'mock author', - 'ORCID': "0000-1111-2222-3333", - } - ], - 'organism': { - 'name': 'mock organism', - } -} - -MOCK_TOMOGRAM_POS_128 = mock_tomogram( - dataset_name="10004", - tomogram_name="Position_128_2", - voxel_spacing="7.56", - annotation_names=("ribosome"), -) - -MOCK_TOMOGRAM_POS_129 = mock_tomogram( - dataset_name="10004", - tomogram_name="Position_129_2", - voxel_spacing="7.56", - annotation_names=("ribosome"), -) - -MOCK_DATASET_10004 = mock_dataset( - name="10004", - tomograms=(MOCK_TOMOGRAM_POS_128, MOCK_TOMOGRAM_POS_129), -) - -MOCK_DATASETS = (MOCK_DATASET_10000, MOCK_DATASET_10004) - - -def mock_path_exists(path: str) -> bool: - return path == MOCK_S3_URI - - -def mock_list_dir(path: str) -> Tuple[str, ...]: - if path == f'{MOCK_S3_URI}': - return tuple(ds.name for ds in MOCK_DATASETS) - for ds in MOCK_DATASETS: - if ds.path == path: - return tuple(tomo.name for tomo in ds.tomograms) - for tomo in ds.tomograms: - anno_path = f"{tomo.tomogram_path}/Annotations" - if path == anno_path: - return tuple( - p.split('/')[-1] - for p in tomo.annotation_paths - ) - raise ValueError(f"Mock path not supported: {path}") - - -def mock_read_json(path: str) -> Dict[str, Any]: - if path == MOCK_DATASET_10000.metadata_path: - return MOCK_DATASET_10000_METADATA - if path == MOCK_TOMOGRAM_TS_026.tomogram_metadata_path: - return MOCK_TOMOGRAM_TS_026_METADATA - raise ValueError(f'Mock path not supported: {path}') - - -def mock_read_tomogram_ome_zarr(path: str) -> Dict[str, Any]: - if path == MOCK_TOMOGRAM_TS_026.image_path: - return MOCK_TOMOGRAM_TS_026_IMAGE_DATA, MOCK_TOMOGRAM_TS_026_IMAGE_ATTRS, "image" - raise ValueError(f'Mock path not supported: {path}') - - -def mock_read_points_annotations_json(path: str) -> Dict[str, Any]: - if path == MOCK_TOMOGRAM_TS_026.annotation_paths[0]: - return MOCK_TOMOGRAM_TS_026_RIBOSOME_DATA, MOCK_TOMOGRAM_TS_026_RIBOSOME_ATTRS, "points" - if path == MOCK_TOMOGRAM_TS_026.annotation_paths[1]: - return MOCK_TOMOGRAM_TS_026_FAS_DATA, MOCK_TOMOGRAM_TS_026_FAS_ATTRS, "points" - raise ValueError(f'Mock path not supported: {path}') diff --git a/src/napari_cryoet_data_portal/_tests/conftest.py b/src/napari_cryoet_data_portal/_tests/conftest.py new file mode 100644 index 0000000..d979be1 --- /dev/null +++ b/src/napari_cryoet_data_portal/_tests/conftest.py @@ -0,0 +1,18 @@ +import pytest + +from cryoet_data_portal import Client, Dataset, Tomogram + + +@pytest.fixture() +def client() -> Client: + return Client() + + +@pytest.fixture() +def dataset(client: Client) -> Dataset: + return next(Dataset.find(client, [Dataset.id == 10000])) + + +@pytest.fixture() +def tomogram(client: Client) -> Tomogram: + return next(Tomogram.find(client, [Tomogram.name == 'TS_026'])) \ No newline at end of file diff --git a/src/napari_cryoet_data_portal/_tests/test_listing_widget.py b/src/napari_cryoet_data_portal/_tests/test_listing_widget.py index 3badfe5..28e40dc 100644 --- a/src/napari_cryoet_data_portal/_tests/test_listing_widget.py +++ b/src/napari_cryoet_data_portal/_tests/test_listing_widget.py @@ -1,15 +1,11 @@ import pytest -from pytest_mock import MockerFixture from pytestqt.qtbot import QtBot +from cryoet_data_portal import Client + from napari_cryoet_data_portal._listing_widget import ListingWidget -from napari_cryoet_data_portal._tests._mocks import ( - MOCK_S3_URI, - mock_list_dir, -) from napari_cryoet_data_portal._tests._utils import ( tree_item_children, - tree_items_names, tree_top_items, ) @@ -30,23 +26,12 @@ def test_init(qtbot: QtBot): assert not widget._progress.isVisibleTo(widget) -def test_load_lists_data(widget: ListingWidget, mocker: MockerFixture, qtbot: QtBot): - mocker.patch( - 'napari_cryoet_data_portal._listing_widget.list_dir', - mock_list_dir, - ) - mocker.patch( - 'napari_cryoet_data_portal._model.list_dir', - mock_list_dir, - ) - - with qtbot.waitSignal(widget._progress.finished): - widget.load(MOCK_S3_URI) +def test_load_lists_data(widget: ListingWidget, client: Client, qtbot: QtBot): + with qtbot.waitSignal(widget._progress.finished, timeout=30000): + widget.load(client) dataset_items = tree_top_items(widget.tree) - assert tree_items_names(dataset_items) == ('10000 (2)', '10004 (2)') - tomogram_items_10000 = tree_item_children(dataset_items[0]) - assert tree_items_names(tomogram_items_10000) == ('TS_026', 'TS_027') - tomogram_items_10001 = tree_item_children(dataset_items[1]) - assert tree_items_names(tomogram_items_10001) == ('Position_128_2', 'Position_129_2') + assert len(dataset_items) > 0 + tomogram_items = tree_item_children(dataset_items[0]) + assert len(tomogram_items) > 0 \ No newline at end of file diff --git a/src/napari_cryoet_data_portal/_tests/test_metadata_widget.py b/src/napari_cryoet_data_portal/_tests/test_metadata_widget.py index e73d220..91eabb9 100644 --- a/src/napari_cryoet_data_portal/_tests/test_metadata_widget.py +++ b/src/napari_cryoet_data_portal/_tests/test_metadata_widget.py @@ -1,13 +1,9 @@ import pytest -from pytest_mock import MockerFixture from pytestqt.qtbot import QtBot +from cryoet_data_portal import Dataset, Tomogram + from napari_cryoet_data_portal._metadata_widget import MetadataWidget -from napari_cryoet_data_portal._tests._mocks import ( - MOCK_DATASET_10000, - MOCK_TOMOGRAM_TS_026, - mock_read_json, -) from napari_cryoet_data_portal._tests._utils import tree_top_items @@ -26,39 +22,17 @@ def test_init(qtbot: QtBot): assert not widget._progress.isVisibleTo(widget) -def test_load_dataset_lists_metadata(widget: MetadataWidget, mocker: MockerFixture, qtbot: QtBot): - mocker.patch( - 'napari_cryoet_data_portal._metadata_widget.read_json', - mock_read_json, - ) - +def test_load_dataset_lists_metadata(widget: MetadataWidget, dataset: Dataset, qtbot: QtBot): with qtbot.waitSignal(widget._progress.finished): - widget.load(MOCK_DATASET_10000) + widget.load(dataset) items = tree_top_items(widget._main.tree) - - assert len(items) == 3 - assert items[0].text(0) == 'dataset_title' - assert items[0].text(1) == 'mock dataset' - assert items[1].text(0) == 'authors' - assert items[2].text(0) == 'organism' + assert len(items) > 0 -def test_load_tomogram_lists_metadata(widget: MetadataWidget, mocker: MockerFixture, qtbot: QtBot): - mocker.patch( - 'napari_cryoet_data_portal._metadata_widget.read_json', - mock_read_json, - ) - +def test_load_tomogram_lists_metadata(widget: MetadataWidget, tomogram: Tomogram, qtbot: QtBot): with qtbot.waitSignal(widget._progress.finished): - widget.load(MOCK_TOMOGRAM_TS_026) + widget.load(tomogram) items = tree_top_items(widget._main.tree) - - assert len(items) == 3 - assert items[0].text(0) == 'run_name' - assert items[0].text(1) == 'TS_026' - assert items[1].text(0) == 'voxel_spacing' - assert items[1].text(1) == '13.48' - assert items[2].text(0) == 'size' - \ No newline at end of file + assert len(items) > 0 \ No newline at end of file diff --git a/src/napari_cryoet_data_portal/_tests/test_model.py b/src/napari_cryoet_data_portal/_tests/test_model.py deleted file mode 100644 index 559e61c..0000000 --- a/src/napari_cryoet_data_portal/_tests/test_model.py +++ /dev/null @@ -1,25 +0,0 @@ -from napari_cryoet_data_portal._model import PORTAL_S3_URI, Dataset, Tomogram - - -def test_dataset_from_data_path_and_name(): - dataset = Dataset.from_data_path_and_name(PORTAL_S3_URI, "10000") - - assert dataset.name == "10000" - assert dataset.path == f"{PORTAL_S3_URI}/10000" - assert len(dataset.tomograms) > 0 - tomogram_names = tuple(s.name for s in dataset.tomograms) - assert "TS_026" in tomogram_names - - -def test_tomogram_from_dataset_path_and_name(): - dataset_dir = f"{PORTAL_S3_URI}/10000" - - tomogram = Tomogram.from_dataset_path_and_name(dataset_dir, "TS_026") - - assert tomogram.name == "TS_026" - assert tomogram.path == f"{dataset_dir}/TS_026" - assert ( - tomogram.image_path - == f"{dataset_dir}/TS_026/Tomograms/VoxelSpacing13.48/CanonicalTomogram/TS_026.zarr" - ) - assert len(tomogram.annotation_paths) > 0 diff --git a/src/napari_cryoet_data_portal/_tests/test_open_widget.py b/src/napari_cryoet_data_portal/_tests/test_open_widget.py index 4d2e205..23390bc 100644 --- a/src/napari_cryoet_data_portal/_tests/test_open_widget.py +++ b/src/napari_cryoet_data_portal/_tests/test_open_widget.py @@ -1,14 +1,10 @@ from napari.components import ViewerModel import pytest -from pytest_mock import MockerFixture from pytestqt.qtbot import QtBot +from cryoet_data_portal import Tomogram + from napari_cryoet_data_portal._open_widget import OpenWidget -from napari_cryoet_data_portal._tests._mocks import ( - MOCK_TOMOGRAM_TS_026, - mock_read_points_annotations_json, - mock_read_tomogram_ome_zarr, -) @pytest.fixture() @@ -30,13 +26,14 @@ def test_init(viewer_model: ViewerModel, qtbot: QtBot): assert not widget._progress.isVisibleTo(widget) -def test_set_tomogram_adds_layers_to_viewer(widget: OpenWidget, mocker: MockerFixture, qtbot: QtBot): - mocker.patch('napari_cryoet_data_portal._open_widget.read_tomogram_ome_zarr', mock_read_tomogram_ome_zarr) - mocker.patch('napari_cryoet_data_portal._open_widget.read_points_annotations_json', mock_read_points_annotations_json) +def test_set_tomogram_adds_layers_to_viewer(widget: OpenWidget, tomogram: Tomogram, qtbot: QtBot): assert len(widget._viewer.layers) == 0 with qtbot.waitSignal(widget._progress.finished): - widget.setTomogram(MOCK_TOMOGRAM_TS_026) + widget.setTomogram(tomogram) - assert len(widget._viewer.layers) == 3 + # TODO: could be more specific, but we currently get more + # points than expected due to the following issue: + # https://github.com/chanzuckerberg/cryoet-data-portal/issues/15 + assert len(widget._viewer.layers) > 1 \ No newline at end of file diff --git a/src/napari_cryoet_data_portal/_tests/test_uri_widget.py b/src/napari_cryoet_data_portal/_tests/test_uri_widget.py index d57efb8..8b77d73 100644 --- a/src/napari_cryoet_data_portal/_tests/test_uri_widget.py +++ b/src/napari_cryoet_data_portal/_tests/test_uri_widget.py @@ -2,11 +2,9 @@ from pytest_mock import MockerFixture from pytestqt.qtbot import QtBot -from napari_cryoet_data_portal._uri_widget import UriWidget -from napari_cryoet_data_portal._tests._mocks import ( - MOCK_S3_URI, - mock_path_exists, -) +from cryoet_data_portal import Client + +from napari_cryoet_data_portal._uri_widget import GRAPHQL_URI, UriWidget @pytest.fixture() @@ -21,36 +19,27 @@ def test_init(qtbot: QtBot): qtbot.add_widget(widget) assert widget._connect_button.isVisibleTo(widget) - assert widget._choose_dir_button.isVisibleTo(widget) assert widget._uri_edit.isVisibleTo(widget) assert not widget._disconnect_button.isVisibleTo(widget) assert not widget._progress.isVisibleTo(widget) def test_click_connect_when_uri_exists(widget: UriWidget, qtbot: QtBot, mocker: MockerFixture): - mocker.patch( - 'napari_cryoet_data_portal._uri_widget.path_exists', - mock_path_exists, - ) - widget._uri_edit.setText(MOCK_S3_URI) + widget._uri_edit.setText(GRAPHQL_URI) with qtbot.waitSignal(widget.connected): widget._connect_button.click() assert not widget._connect_button.isVisibleTo(widget) - assert not widget._choose_dir_button.isVisibleTo(widget) assert widget._uri_edit.isVisibleTo(widget) assert widget._uri_edit.isReadOnly() assert widget._disconnect_button.isVisibleTo(widget) assert not widget._progress.isVisibleTo(widget) +@pytest.mark.skip(reason="https://github.com/chanzuckerberg/cryoet-data-portal/issues/16") def test_click_connect_when_uri_does_not_exist(widget: UriWidget, qtbot: QtBot, mocker: MockerFixture): - mocker.patch( - 'napari_cryoet_data_portal._uri_widget.path_exists', - mock_path_exists, - ) - widget._uri_edit.setText('s3://mock-bad-uri') + widget._uri_edit.setText("https://not.a.graphl.url/v1/graphql") with qtbot.captureExceptions() as exceptions: with qtbot.waitSignal(widget._progress.finished): @@ -59,36 +48,20 @@ def test_click_connect_when_uri_does_not_exist(widget: UriWidget, qtbot: QtBot, assert exceptions[0][0] is ValueError assert widget._connect_button.isVisibleTo(widget) - assert widget._choose_dir_button.isVisibleTo(widget) assert widget._uri_edit.isVisibleTo(widget) assert not widget._uri_edit.isReadOnly() assert not widget._disconnect_button.isVisibleTo(widget) assert not widget._progress.isVisibleTo(widget) -def test_click_disconnect(widget: UriWidget, qtbot: QtBot): - widget._onUriChecked((True, MOCK_S3_URI)) +def test_click_disconnect(widget: UriWidget, client: Client, qtbot: QtBot): + widget._onConnected(client) with qtbot.waitSignal(widget.disconnected): widget._disconnect_button.click() assert widget._connect_button.isVisibleTo(widget) - assert widget._choose_dir_button.isVisibleTo(widget) assert widget._uri_edit.isVisibleTo(widget) assert not widget._uri_edit.isReadOnly() assert not widget._disconnect_button.isVisibleTo(widget) - assert not widget._progress.isVisibleTo(widget) - - -def test_choose_dir_button_clicked(widget: UriWidget, mocker: MockerFixture): - mock_dir = '/path/to/test' - def mock_get_existing_directory(_): - return mock_dir - mocker.patch( - 'napari_cryoet_data_portal._uri_widget.QFileDialog.getExistingDirectory', - mock_get_existing_directory, - ) - - widget._choose_dir_button.click() - - assert widget._uri_edit.text() == mock_dir \ No newline at end of file + assert not widget._progress.isVisibleTo(widget) \ No newline at end of file diff --git a/src/napari_cryoet_data_portal/_tests/test_widget.py b/src/napari_cryoet_data_portal/_tests/test_widget.py index f220bdc..8fb8b0b 100644 --- a/src/napari_cryoet_data_portal/_tests/test_widget.py +++ b/src/napari_cryoet_data_portal/_tests/test_widget.py @@ -1,6 +1,7 @@ from typing import Callable, List, Tuple import pytest +from cryoet_data_portal import Client from napari import Viewer from napari.components import ViewerModel from pytest_mock import MockerFixture @@ -58,13 +59,12 @@ def test_listing_item_changed_to_none(widget: DataPortalWidget): ) -def test_connected_loads_listing(widget: DataPortalWidget, mocker: MockerFixture): +def test_connected_loads_listing(widget: DataPortalWidget, client: Client, mocker: MockerFixture): mocker.patch.object(widget._listing, 'load') - uri = 's3://mock-portal' - widget._uri.connected.emit(uri) + widget._uri.connected.emit(client) - widget._listing.load.assert_called_once_with(uri) + widget._listing.load.assert_called_once_with(client) def test_disconnected_only_shows_uri(widget: DataPortalWidget): diff --git a/src/napari_cryoet_data_portal/_uri_widget.py b/src/napari_cryoet_data_portal/_uri_widget.py index a130d47..8211969 100644 --- a/src/napari_cryoet_data_portal/_uri_widget.py +++ b/src/napari_cryoet_data_portal/_uri_widget.py @@ -2,12 +2,10 @@ from qtpy.QtCore import Signal from qtpy.QtWidgets import ( - QFileDialog, QGroupBox, QHBoxLayout, QLineEdit, QPushButton, - QStyle, QVBoxLayout, QWidget, ) @@ -36,19 +34,14 @@ def __init__(self, parent: Optional[QWidget] = None) -> None: self._uri_edit = QLineEdit(GRAPHQL_URI) self._uri_edit.setCursorPosition(0) self._uri_edit.setPlaceholderText("Enter a URI to CryoET portal data") - choose_dir_icon = self.style().standardIcon( - QStyle.StandardPixmap.SP_DirOpenIcon - ) - self._choose_dir_button = QPushButton(choose_dir_icon, "") self._progress: ProgressWidget = ProgressWidget( - work=self._checkUri, + work=self._connect, returnCallback=self._onConnected, ) self._updateVisibility(False) self._connect_button.clicked.connect(self._onConnectClicked) self._disconnect_button.clicked.connect(self._onDisconnectClicked) - self._choose_dir_button.clicked.connect(self._onChooseDirClicked) self._uri_edit.returnPressed.connect(self._onConnectClicked) control_layout = QHBoxLayout() @@ -56,7 +49,6 @@ def __init__(self, parent: Optional[QWidget] = None) -> None: control_layout.addWidget(self._connect_button) control_layout.addWidget(self._disconnect_button) control_layout.addWidget(self._uri_edit) - control_layout.addWidget(self._choose_dir_button) layout = QVBoxLayout() layout.addLayout(control_layout) @@ -86,11 +78,5 @@ def _onConnected(self, client: Client) -> None: def _updateVisibility(self, uri_exists: bool) -> None: logger.debug("UriWidget._updateVisibility: %s", uri_exists) self._connect_button.setVisible(not uri_exists) - self._choose_dir_button.setVisible(not uri_exists) self._disconnect_button.setVisible(uri_exists) self._uri_edit.setReadOnly(uri_exists) - - def _onChooseDirClicked(self) -> None: - logger.debug("UriWidget._onChooseDirClicked") - path = QFileDialog.getExistingDirectory(self) - self._uri_edit.setText(path) From c627783c8dfbd10c17f73b203c0a4f492b32d5e7 Mon Sep 17 00:00:00 2001 From: Andy Sweet Date: Fri, 30 Jun 2023 16:02:27 -0700 Subject: [PATCH 07/17] Keep using mocks --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 8e2f492..a2b2baf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -59,6 +59,7 @@ testing = tox pytest pytest-cov + pytest-mock pytest-qt napari pyqt5 From 075454ba22fe756e5251fe91b07feaf464960415 Mon Sep 17 00:00:00 2001 From: Andy Sweet Date: Fri, 30 Jun 2023 16:04:17 -0700 Subject: [PATCH 08/17] Remove unneeded mockers --- src/napari_cryoet_data_portal/_tests/test_uri_widget.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/napari_cryoet_data_portal/_tests/test_uri_widget.py b/src/napari_cryoet_data_portal/_tests/test_uri_widget.py index 8b77d73..91e6a41 100644 --- a/src/napari_cryoet_data_portal/_tests/test_uri_widget.py +++ b/src/napari_cryoet_data_portal/_tests/test_uri_widget.py @@ -1,5 +1,4 @@ import pytest -from pytest_mock import MockerFixture from pytestqt.qtbot import QtBot from cryoet_data_portal import Client @@ -24,7 +23,7 @@ def test_init(qtbot: QtBot): assert not widget._progress.isVisibleTo(widget) -def test_click_connect_when_uri_exists(widget: UriWidget, qtbot: QtBot, mocker: MockerFixture): +def test_click_connect_when_uri_exists(widget: UriWidget, qtbot: QtBot): widget._uri_edit.setText(GRAPHQL_URI) with qtbot.waitSignal(widget.connected): @@ -38,7 +37,7 @@ def test_click_connect_when_uri_exists(widget: UriWidget, qtbot: QtBot, mocker: @pytest.mark.skip(reason="https://github.com/chanzuckerberg/cryoet-data-portal/issues/16") -def test_click_connect_when_uri_does_not_exist(widget: UriWidget, qtbot: QtBot, mocker: MockerFixture): +def test_click_connect_when_uri_does_not_exist(widget: UriWidget, qtbot: QtBot): widget._uri_edit.setText("https://not.a.graphl.url/v1/graphql") with qtbot.captureExceptions() as exceptions: From 68915910d8d4166c952b8cb4bf6811b3d68262c7 Mon Sep 17 00:00:00 2001 From: Andy Sweet Date: Fri, 30 Jun 2023 16:06:23 -0700 Subject: [PATCH 09/17] Remove s3fs as an explicit dependency --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index a2b2baf..da832c7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,7 +38,6 @@ install_requires = napari_ome_zarr ndjson qtpy - s3fs superqt python_requires = >=3.8 From e8ace6e613b9a5125b52e9e113f5ff07024a64f5 Mon Sep 17 00:00:00 2001 From: Andy Sweet Date: Wed, 5 Jul 2023 15:19:51 -0700 Subject: [PATCH 10/17] Simplify test --- src/napari_cryoet_data_portal/_tests/test_open_widget.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/napari_cryoet_data_portal/_tests/test_open_widget.py b/src/napari_cryoet_data_portal/_tests/test_open_widget.py index 23390bc..c68fd5f 100644 --- a/src/napari_cryoet_data_portal/_tests/test_open_widget.py +++ b/src/napari_cryoet_data_portal/_tests/test_open_widget.py @@ -32,8 +32,5 @@ def test_set_tomogram_adds_layers_to_viewer(widget: OpenWidget, tomogram: Tomogr with qtbot.waitSignal(widget._progress.finished): widget.setTomogram(tomogram) - # TODO: could be more specific, but we currently get more - # points than expected due to the following issue: - # https://github.com/chanzuckerberg/cryoet-data-portal/issues/15 - assert len(widget._viewer.layers) > 1 + assert len(widget._viewer.layers) == 3 \ No newline at end of file From 8435b99f16e8b809866d8ee2f8f74dab116f894c Mon Sep 17 00:00:00 2001 From: Andy Sweet Date: Wed, 5 Jul 2023 16:25:40 -0700 Subject: [PATCH 11/17] Pass through URI --- src/napari_cryoet_data_portal/_listing_widget.py | 11 ++++++----- src/napari_cryoet_data_portal/_open_widget.py | 13 ++++++++++--- .../_tests/test_listing_widget.py | 7 +++---- .../_tests/test_uri_widget.py | 6 ++---- src/napari_cryoet_data_portal/_tests/test_widget.py | 7 ++++--- src/napari_cryoet_data_portal/_uri_widget.py | 13 +++++++------ src/napari_cryoet_data_portal/_widget.py | 9 +++------ 7 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/napari_cryoet_data_portal/_listing_widget.py b/src/napari_cryoet_data_portal/_listing_widget.py index e5b574f..7592688 100644 --- a/src/napari_cryoet_data_portal/_listing_widget.py +++ b/src/napari_cryoet_data_portal/_listing_widget.py @@ -43,20 +43,21 @@ def __init__(self, parent: Optional[QWidget] = None) -> None: layout.addStretch(0) self.setLayout(layout) - def load(self, client: Client) -> None: + def load(self, uri: str) -> None: """Lists the datasets and tomograms using the given client.""" - logger.debug("ListingWidget.load: %s", client) + logger.debug("ListingWidget.load: %s", uri) self.tree.clear() self.show() - self._progress.submit(client) + self._progress.submit(uri) def cancel(self) -> None: """Cancels the last listing.""" logger.debug("ListingWidget.cancel") self._progress.cancel() - def _loadDatasets(self, client: str) -> Generator[Tuple[Dataset, List[Tomogram]], None, None]: - logger.debug("ListingWidget._loadDatasets: %s", client) + def _loadDatasets(self, uri: str) -> Generator[Tuple[Dataset, List[Tomogram]], None, None]: + logger.debug("ListingWidget._loadDatasets: %s", uri) + client = Client(uri) for dataset in Dataset.find(client): tomograms: List[Tomogram] = [] for run in dataset.runs: diff --git a/src/napari_cryoet_data_portal/_open_widget.py b/src/napari_cryoet_data_portal/_open_widget.py index c02fb33..97a4e95 100644 --- a/src/napari_cryoet_data_portal/_open_widget.py +++ b/src/napari_cryoet_data_portal/_open_widget.py @@ -56,6 +56,7 @@ def __init__( super().__init__(parent) self._viewer = viewer + self._uri: Optional[str] = None self._tomogram: Optional[Tomogram] = None self.setTitle("Tomogram") @@ -88,6 +89,10 @@ def __init__( layout.addWidget(self._progress) self.setLayout(layout) + def setUri(self, uri: str) -> None: + """Sets the URI of the portal that should be used to open data.""" + self._uri = uri + def setTomogram(self, tomogram: Tomogram) -> None: """Sets the current tomogram that should be opened.""" self.cancel() @@ -134,9 +139,11 @@ def _loadTomogram( # Looking up tomogram.tomogram_voxel_spacing.annotations triggers a query # using the client from where the tomogram was found. # A single client is not thread safe, so we need a new instance for each query. - # TODO: pass through URI in setTomogram from main widget. - client = Client() - annotations = Annotation.find(client, [Annotation.tomogram_voxel_spacing_id == tomogram.tomogram_voxel_spacing_id]) + client = Client(self._uri) + annotations = Annotation.find( + client, + [Annotation.tomogram_voxel_spacing_id == tomogram.tomogram_voxel_spacing_id], + ) for annotation in annotations: data, attrs, layer_type = read_annotation_points(annotation) diff --git a/src/napari_cryoet_data_portal/_tests/test_listing_widget.py b/src/napari_cryoet_data_portal/_tests/test_listing_widget.py index 28e40dc..df1b7c2 100644 --- a/src/napari_cryoet_data_portal/_tests/test_listing_widget.py +++ b/src/napari_cryoet_data_portal/_tests/test_listing_widget.py @@ -1,13 +1,12 @@ import pytest from pytestqt.qtbot import QtBot -from cryoet_data_portal import Client - from napari_cryoet_data_portal._listing_widget import ListingWidget from napari_cryoet_data_portal._tests._utils import ( tree_item_children, tree_top_items, ) +from napari_cryoet_data_portal._uri_widget import GRAPHQL_URI @pytest.fixture() @@ -26,9 +25,9 @@ def test_init(qtbot: QtBot): assert not widget._progress.isVisibleTo(widget) -def test_load_lists_data(widget: ListingWidget, client: Client, qtbot: QtBot): +def test_load_lists_data(widget: ListingWidget, qtbot: QtBot): with qtbot.waitSignal(widget._progress.finished, timeout=30000): - widget.load(client) + widget.load(GRAPHQL_URI) dataset_items = tree_top_items(widget.tree) assert len(dataset_items) > 0 diff --git a/src/napari_cryoet_data_portal/_tests/test_uri_widget.py b/src/napari_cryoet_data_portal/_tests/test_uri_widget.py index 91e6a41..a91994e 100644 --- a/src/napari_cryoet_data_portal/_tests/test_uri_widget.py +++ b/src/napari_cryoet_data_portal/_tests/test_uri_widget.py @@ -1,8 +1,6 @@ import pytest from pytestqt.qtbot import QtBot -from cryoet_data_portal import Client - from napari_cryoet_data_portal._uri_widget import GRAPHQL_URI, UriWidget @@ -53,8 +51,8 @@ def test_click_connect_when_uri_does_not_exist(widget: UriWidget, qtbot: QtBot): assert not widget._progress.isVisibleTo(widget) -def test_click_disconnect(widget: UriWidget, client: Client, qtbot: QtBot): - widget._onConnected(client) +def test_click_disconnect(widget: UriWidget, qtbot: QtBot): + widget._onConnected(GRAPHQL_URI) with qtbot.waitSignal(widget.disconnected): widget._disconnect_button.click() diff --git a/src/napari_cryoet_data_portal/_tests/test_widget.py b/src/napari_cryoet_data_portal/_tests/test_widget.py index 8fb8b0b..2643313 100644 --- a/src/napari_cryoet_data_portal/_tests/test_widget.py +++ b/src/napari_cryoet_data_portal/_tests/test_widget.py @@ -9,6 +9,7 @@ from qtpy.QtWidgets import QWidget from napari_cryoet_data_portal import DataPortalWidget +from napari_cryoet_data_portal._uri_widget import GRAPHQL_URI @pytest.fixture() @@ -59,12 +60,12 @@ def test_listing_item_changed_to_none(widget: DataPortalWidget): ) -def test_connected_loads_listing(widget: DataPortalWidget, client: Client, mocker: MockerFixture): +def test_connected_loads_listing(widget: DataPortalWidget, mocker: MockerFixture): mocker.patch.object(widget._listing, 'load') - widget._uri.connected.emit(client) + widget._uri.connected.emit(GRAPHQL_URI) - widget._listing.load.assert_called_once_with(client) + widget._listing.load.assert_called_once_with(GRAPHQL_URI) def test_disconnected_only_shows_uri(widget: DataPortalWidget): diff --git a/src/napari_cryoet_data_portal/_uri_widget.py b/src/napari_cryoet_data_portal/_uri_widget.py index 8211969..915a915 100644 --- a/src/napari_cryoet_data_portal/_uri_widget.py +++ b/src/napari_cryoet_data_portal/_uri_widget.py @@ -20,8 +20,8 @@ class UriWidget(QGroupBox): """Connects to a data portal with a specific URI.""" - # Emitted on successful connection to the URI it contains. - connected = Signal(object) + # Emitted on successful connection with the URI. + connected = Signal(str) # Emitted when disconnecting from the portal. disconnected = Signal() @@ -68,12 +68,13 @@ def _onDisconnectClicked(self) -> None: self.disconnected.emit() def _connect(self, uri: str) -> Client: - return Client(uri) + _ = Client(uri) + return uri - def _onConnected(self, client: Client) -> None: - logger.debug("UriWidget._onConnected: %s", client) + def _onConnected(self, uri: str) -> None: + logger.debug("UriWidget._onConnected: %s", uri) self._updateVisibility(True) - self.connected.emit(client) + self.connected.emit(uri) def _updateVisibility(self, uri_exists: bool) -> None: logger.debug("UriWidget._updateVisibility: %s", uri_exists) diff --git a/src/napari_cryoet_data_portal/_widget.py b/src/napari_cryoet_data_portal/_widget.py index e133e76..00c98f9 100644 --- a/src/napari_cryoet_data_portal/_widget.py +++ b/src/napari_cryoet_data_portal/_widget.py @@ -39,8 +39,6 @@ def __init__( ) -> None: super().__init__(parent) - self._client: Optional[Client] = None - self._uri = UriWidget() self._listing = ListingWidget() @@ -67,14 +65,13 @@ def __init__( self.setLayout(layout) - def _onUriConnected(self, client: Client) -> None: + def _onUriConnected(self, uri: str) -> None: logger.debug("DataPortalWidget._onUriConnected") - self._client = client - self._listing.load(client) + self._open.setUri(uri) + self._listing.load(uri) def _onUriDisconnected(self) -> None: logger.debug("DataPortalWidget._onUriDisconnected") - self._client = None for widget in (self._listing, self._metadata, self._open): widget.cancel() widget.hide() From 84b645b4b8f44b99a60e1a62748d4b5c5884ff29 Mon Sep 17 00:00:00 2001 From: Andy Sweet Date: Wed, 5 Jul 2023 16:29:16 -0700 Subject: [PATCH 12/17] Use tomogram name instead of ID --- src/napari_cryoet_data_portal/_metadata_widget.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/napari_cryoet_data_portal/_metadata_widget.py b/src/napari_cryoet_data_portal/_metadata_widget.py index 58d5896..94bae45 100644 --- a/src/napari_cryoet_data_portal/_metadata_widget.py +++ b/src/napari_cryoet_data_portal/_metadata_widget.py @@ -38,7 +38,8 @@ def load(self, data: Union[Dataset, Tomogram]) -> None: """Loads the JSON metadata of the given dataset or tomogram.""" logger.debug("MetadataWidget.load: %s", data) self._main.tree.clear() - self.setTitle(f"Metadata: {data.id}") + name = data.id if isinstance(data, Dataset) else data.name + self.setTitle(f"Metadata: {name}") self.show() self._progress.submit(data) From 6bf2872daf7607230f1f0b6f840b04c2472ffc21 Mon Sep 17 00:00:00 2001 From: Andy Sweet Date: Wed, 5 Jul 2023 16:38:48 -0700 Subject: [PATCH 13/17] Bring back reader features --- src/napari_cryoet_data_portal/_reader.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/napari_cryoet_data_portal/_reader.py b/src/napari_cryoet_data_portal/_reader.py index 3843433..14432f1 100644 --- a/src/napari_cryoet_data_portal/_reader.py +++ b/src/napari_cryoet_data_portal/_reader.py @@ -10,11 +10,11 @@ OBJECT_COLOR = { - 'ribosome': 'red', - 'ribosome, 80 s': 'red', - 'fatty acid synthase': 'darkblue', + "ribosome": "red", + "ribosome, 80 s": "red", + "fatty acid synthase": "darkblue", } -DEFAULT_OBJECT_COLOR = 'red' +DEFAULT_OBJECT_COLOR = "red" def tomogram_ome_zarr_reader(path: PathOrPaths) -> Optional[ReaderFunction]: @@ -122,6 +122,7 @@ def read_points_annotations_ndjson(path: str) -> FullLayerData: "size": 14, "face_color": "red", "opacity": 0.5, + "out_of_slice_display": True, } return data, attributes, "points" @@ -147,8 +148,10 @@ def read_annotation_points(annotation: Annotation) -> FullLayerData: >>> points = Points(data, **attrs) """ data, attributes, layer_type = read_points_annotations_ndjson(annotation.https_annotations_path) - attributes["name"] = annotation.object_name + name = annotation.object_name + attributes["name"] = name attributes["metadata"] = annotation.to_dict() + attributes["face_color"] = OBJECT_COLOR.get(name.lower(), DEFAULT_OBJECT_COLOR) return data, attributes, layer_type From 4f3d4fb107c44987f094bc1590097b8445490d00 Mon Sep 17 00:00:00 2001 From: Andy Sweet Date: Fri, 7 Jul 2023 11:36:49 -0700 Subject: [PATCH 14/17] Increase timeouts --- src/napari_cryoet_data_portal/_tests/test_listing_widget.py | 2 +- src/napari_cryoet_data_portal/_tests/test_open_widget.py | 2 +- src/napari_cryoet_data_portal/_uri_widget.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/napari_cryoet_data_portal/_tests/test_listing_widget.py b/src/napari_cryoet_data_portal/_tests/test_listing_widget.py index df1b7c2..c28743e 100644 --- a/src/napari_cryoet_data_portal/_tests/test_listing_widget.py +++ b/src/napari_cryoet_data_portal/_tests/test_listing_widget.py @@ -26,7 +26,7 @@ def test_init(qtbot: QtBot): def test_load_lists_data(widget: ListingWidget, qtbot: QtBot): - with qtbot.waitSignal(widget._progress.finished, timeout=30000): + with qtbot.waitSignal(widget._progress.finished, timeout=60000): widget.load(GRAPHQL_URI) dataset_items = tree_top_items(widget.tree) diff --git a/src/napari_cryoet_data_portal/_tests/test_open_widget.py b/src/napari_cryoet_data_portal/_tests/test_open_widget.py index c68fd5f..8aa1b8f 100644 --- a/src/napari_cryoet_data_portal/_tests/test_open_widget.py +++ b/src/napari_cryoet_data_portal/_tests/test_open_widget.py @@ -29,7 +29,7 @@ def test_init(viewer_model: ViewerModel, qtbot: QtBot): def test_set_tomogram_adds_layers_to_viewer(widget: OpenWidget, tomogram: Tomogram, qtbot: QtBot): assert len(widget._viewer.layers) == 0 - with qtbot.waitSignal(widget._progress.finished): + with qtbot.waitSignal(widget._progress.finished, timeout=30000): widget.setTomogram(tomogram) assert len(widget._viewer.layers) == 3 diff --git a/src/napari_cryoet_data_portal/_uri_widget.py b/src/napari_cryoet_data_portal/_uri_widget.py index 915a915..2ef5a36 100644 --- a/src/napari_cryoet_data_portal/_uri_widget.py +++ b/src/napari_cryoet_data_portal/_uri_widget.py @@ -67,7 +67,7 @@ def _onDisconnectClicked(self) -> None: self._updateVisibility(False) self.disconnected.emit() - def _connect(self, uri: str) -> Client: + def _connect(self, uri: str) -> str: _ = Client(uri) return uri From beb881c060d26731a5d774c743c526b76f9ee4b1 Mon Sep 17 00:00:00 2001 From: Andy Sweet Date: Fri, 7 Jul 2023 14:23:16 -0700 Subject: [PATCH 15/17] Simplify reading from portal objects --- setup.cfg | 2 +- src/napari_cryoet_data_portal/__init__.py | 6 +- src/napari_cryoet_data_portal/_open_widget.py | 14 ++--- src/napari_cryoet_data_portal/_reader.py | 63 +++++++++++++++++-- src/napari_cryoet_data_portal/_sample_data.py | 15 ++--- 5 files changed, 72 insertions(+), 28 deletions(-) diff --git a/setup.cfg b/setup.cfg index da832c7..64ab1b8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,7 +32,7 @@ project_urls = packages = find: install_requires = cryoet_data_portal - fsspec[https] + fsspec[http,s3] npe2 numpy napari_ome_zarr diff --git a/src/napari_cryoet_data_portal/__init__.py b/src/napari_cryoet_data_portal/__init__.py index 8fb3e24..9a1fca1 100644 --- a/src/napari_cryoet_data_portal/__init__.py +++ b/src/napari_cryoet_data_portal/__init__.py @@ -4,8 +4,9 @@ __version__ = "unknown" from ._reader import ( points_annotations_reader, - read_annotation_points, + read_annotation, read_points_annotations_ndjson, + read_tomogram, read_tomogram_ome_zarr, tomogram_ome_zarr_reader, ) @@ -14,7 +15,8 @@ __all__ = ( "DataPortalWidget", "points_annotations_reader", - "read_annotation_points", + "read_annotation", + "read_tomogram", "read_tomogram_ome_zarr", "read_points_annotations_ndjson", "tomogram_ome_zarr_reader", diff --git a/src/napari_cryoet_data_portal/_open_widget.py b/src/napari_cryoet_data_portal/_open_widget.py index 97a4e95..1508569 100644 --- a/src/napari_cryoet_data_portal/_open_widget.py +++ b/src/napari_cryoet_data_portal/_open_widget.py @@ -18,8 +18,8 @@ from napari_cryoet_data_portal._logging import logger from napari_cryoet_data_portal._progress_widget import ProgressWidget from napari_cryoet_data_portal._reader import ( - read_annotation_points, - read_tomogram_ome_zarr, + read_annotation, + read_tomogram, ) if TYPE_CHECKING: @@ -39,7 +39,7 @@ class Resolution: MID_RESOLUTION = Resolution(name="Mid", indices=(1,), scale=2) LOW_RESOLUTION = Resolution(name="Low", indices=(2,), scale=4) -RESOLUTIONS: Tuple[Resolution] = ( +RESOLUTIONS: Tuple[Resolution, ...] = ( MULTI_RESOLUTION, HIGH_RESOLUTION, MID_RESOLUTION, @@ -122,8 +122,7 @@ def _loadTomogram( resolution: Resolution, ) -> Generator[FullLayerData, None, None]: logger.debug("OpenWidget._loadTomogram: %s", tomogram.name) - image_data, image_attrs, _ = read_tomogram_ome_zarr(tomogram.https_omezarr_dir) - image_attrs["name"] = f"{tomogram.name}-tomogram" + image_data, image_attrs, _ = read_tomogram(tomogram) # Skip indexing for multi-resolution to avoid adding any # unnecessary nodes to the dask compute graph. if resolution is not MULTI_RESOLUTION: @@ -146,10 +145,7 @@ def _loadTomogram( ) for annotation in annotations: - data, attrs, layer_type = read_annotation_points(annotation) - name = attrs["name"] - attrs["name"] = f"{tomogram.name}-{name}" - yield data, attrs, layer_type + yield read_annotation(annotation, tomogram=tomogram) def _onLayerLoaded(self, layer_data: FullLayerData) -> None: logger.debug("OpenWidget._onLayerLoaded") diff --git a/src/napari_cryoet_data_portal/_reader.py b/src/napari_cryoet_data_portal/_reader.py index 14432f1..a5fb55a 100644 --- a/src/napari_cryoet_data_portal/_reader.py +++ b/src/napari_cryoet_data_portal/_reader.py @@ -6,7 +6,7 @@ import ndjson from napari_ome_zarr import napari_get_reader from npe2.types import FullLayerData, PathOrPaths, ReaderFunction -from cryoet_data_portal import Annotation +from cryoet_data_portal import Annotation, Tomogram OBJECT_COLOR = { @@ -69,7 +69,6 @@ def read_tomogram_ome_zarr(path: str) -> FullLayerData: Examples -------- - >>> from napari.layers import Image >>> path = 's3://cryoet-data-portal-public/10000/TS_026/Tomograms/VoxelSpacing13.48/CanonicalTomogram/TS_026.zarr' >>> data, attrs, _ = read_tomogram_ome_zarr(path) >>> image = Image(data, **attrs) @@ -79,6 +78,33 @@ def read_tomogram_ome_zarr(path: str) -> FullLayerData: return layers[0] +def read_tomogram(tomogram: Tomogram) -> FullLayerData: + """Reads a napari image layer from a tomogram. + + Parameters + ---------- + tomogram : Tomogram + The tomogram to read. + + Returns + ------- + napari layer data tuple + The data, attributes, and type name of the image layer that would be + returned by `Image.as_layer_data_tuple`. + + Examples + -------- + >>> client = Client() + >>> tomogram = client.find_one(Tomogram) + >>> data, attrs, _ = read_tomogram(tomogram) + >>> image = Image(data, **attrs) + """ + data, attributes, layer_type = read_tomogram_ome_zarr(tomogram.https_omezarr_dir) + attributes["name"] = tomogram.name + attributes["metadata"] = tomogram.to_dict() + return data, attributes, layer_type + + def points_annotations_reader(path: PathOrPaths) -> Optional[ReaderFunction]: """napari plugin entry point for reading points annotations. @@ -116,6 +142,25 @@ def _read_many_points_annotations_ndjson(paths: PathOrPaths) -> List[FullLayerDa def read_points_annotations_ndjson(path: str) -> FullLayerData: + """Reads a napari points layer from an NDJSON annotation file. + + Parameters + ---------- + path : str + The path to the NDJSON annotations file. + + Returns + ------- + napari layer data tuple + The data, attributes, and type name of the points layer that would be + returned by `Points.as_layer_data_tuple`. + + Examples + -------- + >>> path = 's3://cryoet-data-portal-public/10000/TS_026/Tomograms/VoxelSpacing13.48/Annotations/sara_goetz-ribosome-1.0.json' + >>> data, attrs, _ = read_points_annotations_ndjson(path) + >>> points = Points(data, **attrs) + """ data = _read_points_data(path) attributes = { "name": "annotations", @@ -127,13 +172,15 @@ def read_points_annotations_ndjson(path: str) -> FullLayerData: return data, attributes, "points" -def read_annotation_points(annotation: Annotation) -> FullLayerData: +def read_annotation(annotation: Annotation, *, tomogram: Optional[Tomogram] = None) -> FullLayerData: """Reads a napari points layer from an annotation. Parameters ---------- annotation : Annotation The tomogram annotation. + tomogram : Tomogram, optional + The associated tomogram, which may be used for other metadata. Returns ------- @@ -143,13 +190,17 @@ def read_annotation_points(annotation: Annotation) -> FullLayerData: Examples -------- - >>> path = 's3://cryoet-data-portal-public/10000/TS_026/Tomograms/VoxelSpacing13.48/Annotations/sara_goetz-ribosome-1.0.json' - >>> data, attrs, _ = read_points_annotations_json(path) + >>> client = Client() + >>> annotation = client.find_one(Annotation) + >>> data, attrs, _ = read_annotation(annotation) >>> points = Points(data, **attrs) """ data, attributes, layer_type = read_points_annotations_ndjson(annotation.https_annotations_path) name = annotation.object_name - attributes["name"] = name + if tomogram is None: + attributes["name"] = name + else: + attributes["name"] = f"{tomogram.name}-{name}" attributes["metadata"] = annotation.to_dict() attributes["face_color"] = OBJECT_COLOR.get(name.lower(), DEFAULT_OBJECT_COLOR) return data, attributes, layer_type diff --git a/src/napari_cryoet_data_portal/_sample_data.py b/src/napari_cryoet_data_portal/_sample_data.py index a8979e7..9ab4dae 100644 --- a/src/napari_cryoet_data_portal/_sample_data.py +++ b/src/napari_cryoet_data_portal/_sample_data.py @@ -5,8 +5,8 @@ from cryoet_data_portal import Client, Tomogram, TomogramVoxelSpacing from napari_cryoet_data_portal import ( - read_annotation_points, - read_tomogram_ome_zarr, + read_annotation, + read_tomogram, ) @@ -28,19 +28,14 @@ def _read_tomogram_from_10000(name: str) -> List[FullLayerData]: tomogram: Tomogram = next(tomogram_spacing.tomograms) - tomogram_image = read_tomogram_ome_zarr(tomogram.https_omezarr_dir) + tomogram_image = read_tomogram(tomogram) # Materialize lowest resolution for speed. tomogram_image = (np.asarray(tomogram_image[0][-1]), *tomogram_image[1:]) tomogram_image[1]["scale"] = (4, 4, 4) - # TODO: fix this in reader or data. - tomogram_image[1]["name"] = "Tomogram" annotations = tuple(tomogram_spacing.annotations) - ribosome_points = read_annotation_points(annotations[0]) - - fatty_acid_points = read_annotation_points(annotations[1]) - # Make different annotations distinctive. - fatty_acid_points[1]["face_color"] = "blue" + ribosome_points = read_annotation(annotations[0], tomogram=tomogram) + fatty_acid_points = read_annotation(annotations[1], tomogram=tomogram) return [ tomogram_image, From 89f1a78c07339ad8faf736f62eb9feb7f83bd93a Mon Sep 17 00:00:00 2001 From: Andy Sweet Date: Fri, 7 Jul 2023 14:31:53 -0700 Subject: [PATCH 16/17] Make URI read-only --- src/napari_cryoet_data_portal/_listing_widget.py | 2 +- src/napari_cryoet_data_portal/_uri_widget.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/napari_cryoet_data_portal/_listing_widget.py b/src/napari_cryoet_data_portal/_listing_widget.py index 7592688..64a2e1c 100644 --- a/src/napari_cryoet_data_portal/_listing_widget.py +++ b/src/napari_cryoet_data_portal/_listing_widget.py @@ -44,7 +44,7 @@ def __init__(self, parent: Optional[QWidget] = None) -> None: self.setLayout(layout) def load(self, uri: str) -> None: - """Lists the datasets and tomograms using the given client.""" + """Lists the datasets and tomograms using the given portal URI.""" logger.debug("ListingWidget.load: %s", uri) self.tree.clear() self.show() diff --git a/src/napari_cryoet_data_portal/_uri_widget.py b/src/napari_cryoet_data_portal/_uri_widget.py index 2ef5a36..d799181 100644 --- a/src/napari_cryoet_data_portal/_uri_widget.py +++ b/src/napari_cryoet_data_portal/_uri_widget.py @@ -32,6 +32,10 @@ def __init__(self, parent: Optional[QWidget] = None) -> None: self._connect_button = QPushButton("Connect") self._disconnect_button = QPushButton("Disconnect") self._uri_edit = QLineEdit(GRAPHQL_URI) + # Only allow the default portal URI because invalid ones will cause + # indefinite hangs: + # https://github.com/chanzuckerberg/cryoet-data-portal/issues/16 + self._uri_edit.setReadOnly(True) self._uri_edit.setCursorPosition(0) self._uri_edit.setPlaceholderText("Enter a URI to CryoET portal data") self._progress: ProgressWidget = ProgressWidget( @@ -80,4 +84,3 @@ def _updateVisibility(self, uri_exists: bool) -> None: logger.debug("UriWidget._updateVisibility: %s", uri_exists) self._connect_button.setVisible(not uri_exists) self._disconnect_button.setVisible(uri_exists) - self._uri_edit.setReadOnly(uri_exists) From 65d0e09ab71d87b1a7f6de6e48b9a769443ad0ca Mon Sep 17 00:00:00 2001 From: Andy Sweet Date: Fri, 7 Jul 2023 14:45:12 -0700 Subject: [PATCH 17/17] Fix read only in tests --- src/napari_cryoet_data_portal/_tests/test_uri_widget.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/napari_cryoet_data_portal/_tests/test_uri_widget.py b/src/napari_cryoet_data_portal/_tests/test_uri_widget.py index a91994e..1366c20 100644 --- a/src/napari_cryoet_data_portal/_tests/test_uri_widget.py +++ b/src/napari_cryoet_data_portal/_tests/test_uri_widget.py @@ -29,7 +29,6 @@ def test_click_connect_when_uri_exists(widget: UriWidget, qtbot: QtBot): assert not widget._connect_button.isVisibleTo(widget) assert widget._uri_edit.isVisibleTo(widget) - assert widget._uri_edit.isReadOnly() assert widget._disconnect_button.isVisibleTo(widget) assert not widget._progress.isVisibleTo(widget) @@ -46,7 +45,6 @@ def test_click_connect_when_uri_does_not_exist(widget: UriWidget, qtbot: QtBot): assert widget._connect_button.isVisibleTo(widget) assert widget._uri_edit.isVisibleTo(widget) - assert not widget._uri_edit.isReadOnly() assert not widget._disconnect_button.isVisibleTo(widget) assert not widget._progress.isVisibleTo(widget) @@ -59,6 +57,5 @@ def test_click_disconnect(widget: UriWidget, qtbot: QtBot): assert widget._connect_button.isVisibleTo(widget) assert widget._uri_edit.isVisibleTo(widget) - assert not widget._uri_edit.isReadOnly() assert not widget._disconnect_button.isVisibleTo(widget) assert not widget._progress.isVisibleTo(widget) \ No newline at end of file