Repository: AllenCellModeling/napari-aicsimageio (language: python)

Issue:
Error while enabling the Plugin in Napari



Discussion / hints:
Hi, 

Ive got a problem.

I can successfully download the napery-aicsimageio plugin (v0.2.0) via pip. After starting napari, I try to tick the plugins in the plug in menu (they appear there as installed.) But then an error pops up, I attached the log file.


Best,

Malte
[error.log](https://github.com/AllenCellModeling/napari-aicsimageio/files/6638320/error.log)

Hey! Sorry about this. It's because we released `aicsimageio==4.0.0` and didn't update the version pin on this. If you install `aicsimageio==3.3.4` you should be all set to go. I will try to release a new version of this that pins below v4.
Oh maybe not the same issue....

To be honest I want to entirely rewrite this plugin and haven't actively been developing it or maitaining it in a while. I made a fork for new v1.0 work that will pick up with aicsimageio v4 but that is also under-developed at the moment.

I am not sure when I will actively look into this.
Thanks for your fast reply!!

Okay, I need to analyse Zeiss leightsheet microscopy data. Any suggestion in how to tackle this under these circumatances?


I would say trying just `aicsimageio[czi]>=4.0.0`

Here is the readme: https://github.com/AllenCellModeling/aicsimageio

I would recommend reading the file with aicsimageio and then in python passing the numpy or dask array to `napari.view_image`

```python

from aicsimageio import AICSImage
import napari

with napari.gui_qt():
    img = AICSImage("your-file.czi")
    napari.view_image(img.data)  # in memory
    # napari.view_image(img.dask_data)  # delayed chunk reading
```

Note that we have had issues with light sheet data so hard to say really.

Fix patch:
diff --git a/.github/workflows/build-master.yml b/.github/workflows/build-main.yml
similarity index 51%
rename from .github/workflows/build-master.yml
rename to .github/workflows/build-main.yml
index 938264f..ee812f1 100644
--- a/.github/workflows/build-master.yml
+++ b/.github/workflows/build-main.yml
@@ -1,22 +1,28 @@
-name: Build Master
+name: Build Main
 
 on:
   push:
     branches:
-      - master
+      - main
   schedule:
     # <minute [0,59]> <hour [0,23]> <day of the month [1,31]> <month of the year [1,12]> <day of the week [0,6]>
     # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07
     # Run every Monday at 18:00:00 UTC (Monday at 10:00:00 PST)
-    - cron: '0 18 * * 1'
+    - cron: "0 18 * * 1"
 
 jobs:
   test:
     runs-on: ${{ matrix.os }}
     strategy:
+      fail-fast: false
       matrix:
-        python-version: [3.7, 3.8, 3.9]
-        os: [ubuntu-latest, windows-latest, macOS-latest]
+        python-version: [3.8, 3.9]
+        os: [
+          ubuntu-18.04,
+          ubuntu-20.04,
+          windows-latest,
+          macOS-10.15,
+        ]
 
     steps:
     - uses: actions/checkout@v1
@@ -28,10 +34,14 @@ jobs:
       run: |
         python -m pip install --upgrade pip
         pip install .[test]
+    - name: Download Test Resources
+      run: |
+        python scripts/download_test_resources.py
     - name: Test with pytest
       run: |
         pytest --cov-report xml --cov=napari_aicsimageio napari_aicsimageio/tests/
-        codecov -t ${{ secrets.CODECOV_TOKEN }}
+    - name: Upload codecov
+      uses: codecov/codecov-action@v1
 
   lint:
     runs-on: ubuntu-latest
@@ -49,6 +59,30 @@ jobs:
     - name: Lint with flake8
       run: |
         flake8 napari_aicsimageio --count --verbose --show-source --statistics
-    - name: Check with black
+        isort napari_aicsimageio --check-only
+        mypy napari_aicsimageio
+        black --check --exclude vendor napari_aicsimageio
+
+  publish:
+    if: "contains(github.event.head_commit.message, 'Bump version')"
+    needs: [test, lint]
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v1
+    - name: Set up Python
+      uses: actions/setup-python@v1
+      with:
+        python-version: 3.9
+    - name: Install Dependencies
       run: |
-        black --check napari_aicsimageio
+        python -m pip install --upgrade pip
+        pip install setuptools wheel
+    - name: Build Package
+      run: |
+        python setup.py sdist bdist_wheel
+    - name: Publish to PyPI
+      uses: pypa/gh-action-pypi-publish@master
+      with:
+        user: aicspypi
+        password: ${{ secrets.PYPI_TOKEN }}
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
deleted file mode 100644
index fe45793..0000000
--- a/.github/workflows/publish.yml
+++ /dev/null
@@ -1,28 +0,0 @@
-name: Publish
-
-on:
-  push:
-    branches:
-      - stable
-
-jobs:
-  publish:
-    runs-on: ubuntu-latest
-    steps:
-    - uses: actions/checkout@v1
-    - name: Set up Python
-      uses: actions/setup-python@v1
-      with:
-        python-version: 3.9
-    - name: Install Dependencies
-      run: |
-        python -m pip install --upgrade pip
-        pip install setuptools wheel
-    - name: Build Package
-      run: |
-        python setup.py sdist bdist_wheel
-    - name: Publish to PyPI
-      uses: pypa/gh-action-pypi-publish@master
-      with:
-        user: aicspypi
-        password: ${{ secrets.PYPI_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 19adb91..b48d2cb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -69,6 +69,9 @@ instance/
 docs/_build/
 docs/napari_aicsimageio.*rst
 
+# Dask workers
+dask-worker-space/
+
 # PyBuilder
 target/
 
@@ -81,9 +84,6 @@ target/
 # celery beat schedule file
 celerybeat-schedule
 
-# Dask
-dask-worker-space
-
 # SageMath parsed files
 *.sage.py
 
@@ -99,11 +99,22 @@ ENV/
 .spyderproject
 .spyproject
 
+# Pycharm project stuff
+.idea
 # Rope project settings
 .ropeproject
+# VSCode
+.vscode
 
 # mkdocs documentation
 /site
 
 # mypy
 .mypy_cache/
+
+# Project specific standalone files
+workbench.ipynb
+
+# Ignore test resources directory
+resources/
+writer_products/
\ No newline at end of file
diff --git a/.napari/DESCRIPTION.md b/.napari/DESCRIPTION.md
new file mode 100644
index 0000000..2dda101
--- /dev/null
+++ b/.napari/DESCRIPTION.md
@@ -0,0 +1,51 @@
+## Features
+* Supports reading metadata and imaging data for:
+    * `CZI`
+    * `OME-TIFF`
+    * `TIFF`
+    * Any formats supported by [aicsimageio](https://github.com/AllenCellModeling/aicsimageio)
+    * Any additional format supported by [imageio](https://github.com/imageio/imageio)
+
+### Plugin Variants
+
+![screenshot of plugin sorter showing that napari-aicsimageio-in-memory should be placed above napari-aicsimageio-out-of-memory](https://github.com/AllenCellModeling/napari-aicsimageio/tree/main/images/plugin-sorter.png)
+
+There are two variants of this plugin that are added during installation:
+* `aicsimageio-in-memory`, which reads an image fully into memory
+* `aicsimageio-out-of-memory`, which delays reading ZYX chunks until required.
+This allows for incredible large files to be read and displayed.
+
+## Examples of Features
+
+#### General Image Reading
+
+All image file formats supported by
+[aicsimageio](https://github.com/AllenCellModeling/aicsimageio) will be read and all
+raw data will be available in the napari viewer.
+
+In addition, when reading an OME-TIFF, you can view all OME metadata directly in the
+napari viewer thanks to `ome-types`.
+
+![screenshot of an OME-TIFF image view, multi-channel, z-stack, with metadata viewer](https://github.com/AllenCellModeling/napari-aicsimageio/tree/main/images/ome-tiff-with-metadata-viewer.png)
+
+#### Mosaic CZI Reading
+
+When reading CZI images, if the image is a mosaic tiled image, `napari-aicsimageio`
+will return the reconstructed image:
+
+![screenshot of a reconstructed / restitched mosaic tile CZI](https://github.com/AllenCellModeling/napari-aicsimageio/tree/main/images/tiled-czi.png)
+
+#### Mosaic LIF Reading
+
+When reading LIF images, if the image is a mosaic tiled image, `napari-aicsimageio`
+will return the reconstructed image:
+
+![screenshot of a reconstructed / restitched mosaic tile LIF](https://github.com/AllenCellModeling/napari-aicsimageio/tree/main/images/tiled-lif.png)
+
+## Citation
+
+If you find `aicsimageio` _(or `napari-aicsimageio`)_ useful, please cite as:
+
+> AICSImageIO Contributors (2021). AICSImageIO: Image Reading, Metadata Conversion, and Image Writing for Microscopy Images in Pure Python [Computer software]. GitHub. https://github.com/AllenCellModeling/aicsimageio
+
+_Free software: BSD-3-Clause_
\ No newline at end of file
diff --git a/.napari/config.yml b/.napari/config.yml
new file mode 100644
index 0000000..1833945
--- /dev/null
+++ b/.napari/config.yml
@@ -0,0 +1,5 @@
+project_urls:
+  Bug Tracker: https://github.com/AllenCellModeling/napari-aicsimageio/issues
+  Documentation: https://github.com/AllenCellModeling/napari-aicsimageio#README.md
+  Source Code: https://github.com/AllenCellModeling/napari-aicsimageio
+  User Support: https://github.com/AllenCellModeling/napari-aicsimageio/issues
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index fa15a54..d03b0ec 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -30,14 +30,20 @@ Ready to contribute? Here's how to set up `napari-aicsimageio` for local develop
     Ex: feature/read-tiff-files or bugfix/handle-file-not-found<br>
     Now you can make your changes locally.
 
-5. When you're done making changes, check that your changes pass linting and
+5. Download test resources:
+
+   ```bash
+   python scripts/download_test_resources.py
+   ```
+
+6. When you're done making changes, check that your changes pass linting and
    tests, including testing other Python versions with make:
 
     ```bash
     make build
     ```
 
-6. Commit your changes and push your branch to GitHub:
+7. Commit your changes and push your branch to GitHub:
 
     ```bash
     git add .
@@ -45,7 +51,7 @@ Ready to contribute? Here's how to set up `napari-aicsimageio` for local develop
     git push origin {your_development_type}/short-description
     ```
 
-7. Submit a pull request through the GitHub website.
+8. Submit a pull request through the GitHub website.
 
 ## Deploying
 
@@ -57,9 +63,6 @@ Then run:
 $ bumpversion patch # possible: major / minor / patch
 $ git push
 $ git push --tags
-git branch -D stable
-git checkout -b stable
-git push --set-upstream origin stable -f
 ```
 
 This will release a new package version on Git + GitHub and publish to PyPI.
diff --git a/Makefile b/Makefile
index d8ade44..9e86522 100644
--- a/Makefile
+++ b/Makefile
@@ -1,18 +1,6 @@
-.PHONY: clean clean-test clean-pyc clean-build docs help
+.PHONY: clean build help
 .DEFAULT_GOAL := help
 
-define BROWSER_PYSCRIPT
-import os, webbrowser, sys
-
-try:
-	from urllib import pathname2url
-except:
-	from urllib.request import pathname2url
-
-webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1])))
-endef
-export BROWSER_PYSCRIPT
-
 define PRINT_HELP_PYSCRIPT
 import re, sys
 
@@ -24,8 +12,6 @@ for line in sys.stdin:
 endef
 export PRINT_HELP_PYSCRIPT
 
-BROWSER := python -c "$$BROWSER_PYSCRIPT"
-
 help:
 	@python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
 
@@ -34,10 +20,10 @@ clean:  ## clean all build, python, and testing files
 	rm -fr dist/
 	rm -fr .eggs/
 	find . -name '*.egg-info' -exec rm -fr {} +
-	find . -name '*.egg' -exec rm -f {} +
-	find . -name '*.pyc' -exec rm -f {} +
-	find . -name '*.pyo' -exec rm -f {} +
-	find . -name '*~' -exec rm -f {} +
+	find . -name '*.egg' -exec rm -fr {} +
+	find . -name '*.pyc' -exec rm -fr {} +
+	find . -name '*.pyo' -exec rm -fr {} +
+	find . -name '*~' -exec rm -fr {} +
 	find . -name '__pycache__' -exec rm -fr {} +
 	rm -fr .tox/
 	rm -fr .coverage
@@ -46,14 +32,4 @@ clean:  ## clean all build, python, and testing files
 	rm -fr .pytest_cache
 
 build: ## run tox / run tests and lint
-	tox
-
-gen-docs: ## generate Sphinx HTML documentation, including API docs
-	rm -f docs/napari_aicsimageio*.rst
-	rm -f docs/modules.rst
-	sphinx-apidoc -o docs/ napari_aicsimageio **/tests/
-	$(MAKE) -C docs html
-
-docs: ## generate Sphinx HTML documentation, including API docs, and serve to browser
-	make gen-docs
-	$(BROWSER) docs/_build/html/index.html
+	tox
\ No newline at end of file
diff --git a/README.md b/README.md
index 8018db8..444ffdb 100644
--- a/README.md
+++ b/README.md
@@ -14,15 +14,59 @@ AICSImageIO bindings for napari
     * `TIFF`
     * Any formats supported by [aicsimageio](https://github.com/AllenCellModeling/aicsimageio)
     * Any additional format supported by [imageio](https://github.com/imageio/imageio)
-* Two variants of the AICSImageIO bindings:
-    * `aicsimageio`, which reads the image fully into memory
-    * `aicsimageio-delayed`, which delays reading YX planes until requested for large file support
 
 ## Installation
 **Stable Release:** `pip install napari-aicsimageio`<br>
 **Development Head:** `pip install git+https://github.com/AllenCellModeling/napari-aicsimageio.git`
 
+### Plugin Variants
+
+![screenshot of plugin sorter showing that napari-aicsimageio-in-memory should be placed above napari-aicsimageio-out-of-memory](./images/plugin-sorter.png)
+
+There are two variants of this plugin that are added during installation:
+* `aicsimageio-in-memory`, which reads an image fully into memory
+* `aicsimageio-out-of-memory`, which delays reading ZYX chunks until required.
+This allows for incredible large files to be read and displayed.
+
+## Examples of Features
+
+#### General Image Reading
+
+All image file formats supported by
+[aicsimageio](https://github.com/AllenCellModeling/aicsimageio) will be read and all
+raw data will be available in the napari viewer.
+
+In addition, when reading an OME-TIFF, you can view all OME metadata directly in the
+napari viewer thanks to `ome-types`.
+
+![screenshot of an OME-TIFF image view, multi-channel, z-stack, with metadata viewer](./images/ome-tiff-with-metadata-viewer.png)
+
+#### Mosaic CZI Reading
+
+When reading CZI images, if the image is a mosaic tiled image, `napari-aicsimageio`
+will return the reconstructed image:
+
+![screenshot of a reconstructed / restitched mosaic tile CZI](./images/tiled-czi.png)
+
+#### Mosaic LIF Reading
+
+When reading LIF images, if the image is a mosaic tiled image, `napari-aicsimageio`
+will return the reconstructed image:
+
+![screenshot of a reconstructed / restitched mosaic tile LIF](./images/tiled-lif.png)
+
 ## Development
 See [CONTRIBUTING.md](CONTRIBUTING.md) for information related to developing the code.
 
-***Free software: BSD-3-Clause***
+For additional file format support, contributed directly to
+[AICSImageIO](https://github.com/AllenCellModeling/aicsimageio).
+New file format support will become directly available in this
+plugin on new `aicsimageio` releases.
+
+## Citation
+
+If you find `aicsimageio` _(or `napari-aicsimageio`)_ useful, please cite as:
+
+> AICSImageIO Contributors (2021). AICSImageIO: Image Reading, Metadata Conversion, and Image Writing for Microscopy Images in Pure Python [Computer software]. GitHub. https://github.com/AllenCellModeling/aicsimageio
+
+_Free software: BSD-3-Clause_
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..9993df3
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,7 @@
+ignore:
+    - "napari_aicsimageio/vendor/.*"
+    - "**/__init__.py"
+
+coverage:
+  status:
+    patch: off
diff --git a/images/ome-tiff-with-metadata-viewer.png b/images/ome-tiff-with-metadata-viewer.png
new file mode 100644
index 0000000..4ed466b
Binary files /dev/null and b/images/ome-tiff-with-metadata-viewer.png differ
diff --git a/images/plugin-sorter.png b/images/plugin-sorter.png
new file mode 100644
index 0000000..7868d7a
Binary files /dev/null and b/images/plugin-sorter.png differ
diff --git a/images/tiled-czi.png b/images/tiled-czi.png
new file mode 100644
index 0000000..0b4b10a
Binary files /dev/null and b/images/tiled-czi.png differ
diff --git a/images/tiled-lif.png b/images/tiled-lif.png
new file mode 100644
index 0000000..1a845b7
Binary files /dev/null and b/images/tiled-lif.png differ
diff --git a/napari_aicsimageio/__init__.py b/napari_aicsimageio/__init__.py
index 6ddfbf6..0473bf2 100644
--- a/napari_aicsimageio/__init__.py
+++ b/napari_aicsimageio/__init__.py
@@ -9,5 +9,5 @@
 __version__ = "0.2.0"
 
 
-def get_module_version():
+def get_module_version() -> str:
     return __version__
diff --git a/napari_aicsimageio/core.py b/napari_aicsimageio/core.py
index 898469e..93f02b3 100644
--- a/napari_aicsimageio/core.py
+++ b/napari_aicsimageio/core.py
@@ -4,84 +4,120 @@
 from functools import partial
 from typing import Any, Callable, Dict, List, Optional, Tuple, Union
 
-import dask.array as da
-import numpy as np
-from aicsimageio import AICSImage, exceptions
-from aicsimageio.constants import Dimensions
+import xarray as xr
+from aicsimageio import AICSImage, exceptions, types
+from aicsimageio.dimensions import DimensionNames
 
 ###############################################################################
 
-LayerData = Union[Tuple[Any], Tuple[Any, Dict], Tuple[Any, Dict, str]]
+LayerData = Union[Tuple[types.ArrayLike, Dict[str, Any], str]]
 PathLike = Union[str, List[str]]
 ReaderFunction = Callable[[PathLike], List[LayerData]]
 
 ###############################################################################
 
 
-def reader_function(path: PathLike, in_memory: bool) -> List[LayerData]:
+def _get_full_image_data(img: AICSImage, in_memory: bool) -> Optional[xr.DataArray]:
+    if DimensionNames.MosaicTile in img.reader.dims.order:
+        try:
+            if in_memory:
+                return img.reader.mosaic_xarray_data
+            else:
+                return img.reader.mosaic_xarray_dask_data
+
+        # Catch reader does not support tile stitching
+        except NotImplementedError:
+            print(
+                "AICSImageIO: Mosaic tile stitching "
+                "not yet supported for this file format reader."
+            )
+
+    else:
+        if in_memory:
+            return img.reader.xarray_data
+        else:
+            return img.reader.xarray_dask_data
+
+    return None
+
+
+def _get_meta(img: AICSImage) -> Any:
+    """
+    This return type should change in the future to always return OME from ome-types.
+    """
+    return img.metadata
+
+
+def reader_function(
+    path: PathLike, in_memory: bool, scene_name: Optional[str] = None
+) -> Optional[List[LayerData]]:
     """
     Given a single path return a list of LayerData tuples.
     """
     # Alert console of how we are loading the image
-    print(f"Reader will load image in-memory: {in_memory}")
-
-    # Standardize path to list of paths
-    paths = [path] if isinstance(path, str) else path
+    print(f"AICSImageIO: Reader will load image in-memory: {in_memory}")
 
-    # Determine reader for all
-    ReaderClass = AICSImage.determine_reader(paths[0])
+    # Only support single path
+    if isinstance(path, list):
+        print("AICSImageIO: Multi-file reading not yet supported.")
+        return None
 
-    # Create readers for each path
-    readers = [ReaderClass(path) for path in paths]
+    # Open file and get data
+    img = AICSImage(path)
+    print(
+        f"AICSImageIO: Image contains {len(img.scenes)} scenes. "
+        f"napari-aicsimageio currently only supports loading first scene, "
+        f"will load scene: '{img.current_scene}'."
+    )
 
-    # Read every file or create delayed arrays
-    if in_memory:
-        data = [reader.data for reader in readers]
-        data = np.stack(data)
+    data = _get_full_image_data(img, in_memory=in_memory)
 
+    # Catch None data
+    if data is None:
+        return None
     else:
-        data = [reader.dask_data for reader in readers]
-        data = da.stack(data)
-
-    # Construct empty metadata to pass through
-    meta = {}
+        # Metadata to provide with data
+        meta = {}
+        if DimensionNames.Channel in data.dims:
+            # Construct basic metadata
+            meta["name"] = data.coords[DimensionNames.Channel].data.tolist()
+            meta["channel_axis"] = data.dims.index(DimensionNames.Channel)
 
-    # If multiple files were read we need to increment channel axis due to stack
-    # But we only do this is the channel axis isn't single to begin with
-    img_contains_channels = Dimensions.Channel in readers[0].dims
-    if img_contains_channels:
-        # Get channel names for display
-        channel_names = readers[0].get_channel_names()
+        # Not multi-channel, use current scene as image name
+        else:
+            meta["name"] = img.reader.current_scene
 
-        # Fix channel axis in the case of squeezed or many image stack
-        channel_axis = readers[0].dims.index(Dimensions.Channel)
-        channel_axis += 1
+        # Handle samples / RGB
+        if DimensionNames.Samples in img.reader.dims.order:
+            meta["rgb"] = True
 
-        # Construct basic metadata
-        meta["name"] = channel_names
-        meta["channel_axis"] = channel_axis
+        # Apply all other metadata
+        meta_reader = partial(_get_meta, img=img)
+        meta["metadata"] = meta_reader
 
-    return [(data, meta, "image")]
+        return [(data.data, meta, "image")]
 
 
 def get_reader(path: PathLike, in_memory: bool) -> Optional[ReaderFunction]:
     """
     Given a single path or list of paths, return the appropriate aicsimageio reader.
     """
-    # Standardize path to list of paths
-    paths = [path] if isinstance(path, str) else path
+    # Only support single path
+    if isinstance(path, list):
+        print("AICSImageIO: Multi-file reading not yet supported.")
 
     # See if there is a supported reader for the file(s) provided
     try:
         # There is an assumption that the images are stackable and
         # I think it is also safe to assume that if stackable, they are of the same type
         # So only determine reader for the first one
-        AICSImage.determine_reader(paths[0])
+        AICSImage.determine_reader(path)
 
         # The above line didn't error so we know we have a supported reader
         # Return a partial function with in_memory determined
-        return partial(reader_function, in_memory=in_memory)
+        return partial(reader_function, in_memory=in_memory)  # type: ignore
 
     # No supported reader, return None
     except exceptions.UnsupportedFileFormatError:
+        print("AICSImageIO: Unsupported file format.")
         return None
diff --git a/napari_aicsimageio/delayed.py b/napari_aicsimageio/out_of_memory.py
similarity index 100%
rename from napari_aicsimageio/delayed.py
rename to napari_aicsimageio/out_of_memory.py
diff --git a/scripts/TEST_RESOURCES_HASH.txt b/scripts/TEST_RESOURCES_HASH.txt
new file mode 100644
index 0000000..10f84b3
--- /dev/null
+++ b/scripts/TEST_RESOURCES_HASH.txt
@@ -0,0 +1,1 @@
+9b89503baf2bd3520be9223b25a2d500761cf8defcd67523e4ca3e388a4d03b1
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index d3f2976..dbe9096 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,11 +1,13 @@
 [bumpversion]
 current_version = 0.2.0
+parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
+serialize = {major}.{minor}.{patch}
 commit = True
 tag = True
 
 [bumpversion:file:setup.py]
-search = {current_version}
-replace = {new_version}
+search = version="{current_version}"
+replace = version="{new_version}"
 
 [bumpversion:file:napari_aicsimageio/__init__.py]
 search = {current_version}
@@ -19,13 +21,31 @@ test = pytest
 
 [tool:pytest]
 collect_ignore = ['setup.py']
+xfail_strict = true
+filterwarnings = 
+	ignore::UserWarning
+	ignore::FutureWarning
 
 [flake8]
 exclude = 
 	docs/
+	napari_aicsimageio/vendor/
 ignore = 
 	E203
 	E402
 	W291
 	W503
 max-line-length = 88
+
+[isort]
+multi_line_output = 3
+include_trailing_comma = True
+force_grid_wrap = 0
+use_parentheses = True
+ensure_newline_before_comments = True
+line_length = 88
+
+[mypy]
+ignore_missing_imports = True
+disallow_untyped_defs = True
+check_untyped_defs = True
diff --git a/setup.py b/setup.py
index cb1c64b..ec48e9d 100644
--- a/setup.py
+++ b/setup.py
@@ -15,45 +15,40 @@
 test_requirements = [
     "black>=19.10b0",
     "codecov>=2.1.4",
+    "docutils>=0.10,<0.16",
     "flake8>=3.8.3",
     "flake8-debugger>=3.2.1",
-    "napari[pyqt5]>=0.3.0",
+    "isort>=5.7.0",
+    "mypy>=0.800",
+    "psutil>=5.7.0",
     "pytest>=5.4.3",
     "pytest-cov>=2.9.0",
     "pytest-raises>=0.11",
+    "quilt3~=3.4.0",
 ]
 
 dev_requirements = [
     *setup_requirements,
     *test_requirements,
-    "bumpversion>=0.6.0",
+    "bump2version>=1.0.1",
     "coverage>=5.1",
     "ipython>=7.15.0",
-    "m2r>=0.2.1",
     "pytest-runner>=5.2",
-    "Sphinx>=2.0.0b1,<3",
-    "sphinx_rtd_theme>=0.4.3",
     "tox>=3.15.2",
     "twine>=3.1.1",
     "wheel>=0.34.2",
 ]
 
 requirements = [
-    "aicsimageio>=3.3.3",
-    # napari plugins is the `napari>=0.3.0` release
-    # If someone is trying to use plugins they should have everything they need
-    # for this package to work
-    "napari_plugin_engine>=0.1.4",
+    "aicsimageio[all]~=4.0.2",
+    "napari~=0.4.10",
+    "napari_plugin_engine~=0.1.4",
 ]
 
 extra_requirements = {
     "setup": setup_requirements,
     "test": test_requirements,
     "dev": dev_requirements,
-    "all": [
-        *requirements,
-        *dev_requirements,
-    ],
 }
 
 setup(
@@ -69,15 +64,17 @@
         "Topic :: Scientific/Engineering :: Bio-Informatics",
         "License :: OSI Approved :: BSD License",
         "Natural Language :: English",
-        "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
     ],
-    description="AICSImageIO bindings for napari",
+    description=(
+        "AICSImageIO for napari. "
+        "Multiple file format reading directly into napari using pure Python."
+    ),
     entry_points={
         "napari.plugin": [
-            "aicsimageio = napari_aicsimageio.in_memory",
-            "aicsimageio_delayed = napari_aicsimageio.delayed",
+            "aicsimageio-out-of-memory = napari_aicsimageio.out_of_memory",
+            "aicsimageio-in-memory = napari_aicsimageio.in_memory",
         ],
     },
     install_requires=requirements,
@@ -85,10 +82,10 @@
     long_description=readme,
     long_description_content_type="text/markdown",
     include_package_data=True,
-    keywords="napari, aicsimageio, imaging",
+    keywords="napari, aicsimageio, TIFF, CZI, LIF, imageio, image reading, metadata",
     name="napari-aicsimageio",
     packages=find_packages(exclude=["tests", "*.tests", "*.tests.*"]),
-    python_requires=">=3.7",
+    python_requires=">=3.8",
     setup_requires=setup_requirements,
     test_suite="napari_aicsimageio/tests",
     tests_require=test_requirements,
diff --git a/tox.ini b/tox.ini
index 54e16fc..98c4577 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,12 +1,14 @@
 [tox]
 skipsdist = True
-envlist = py37, py38, py39, lint
+envlist = py38, py39, lint
 
 [testenv:lint]
 deps =
     .[test]
 commands =
     flake8 napari_aicsimageio --count --verbose --show-source --statistics
+    isort napari_aicsimageio --check-only
+    mypy napari_aicsimageio
     black --check napari_aicsimageio
 
 [testenv]
@@ -15,4 +17,4 @@ setenv =
 deps =
     .[test]
 commands =
-    pytest --basetemp={envtmpdir} --cov-report html --cov=napari_aicsimageio napari_aicsimageio/tests/
+    pytest --basetemp={envtmpdir} --cov-report html --cov=napari_aicsimageio napari_aicsimageio/tests/
\ No newline at end of file


Test patch:
diff --git a/.github/workflows/test-and-lint.yml b/.github/workflows/test-and-lint.yml
index c45a04d..9d1f9be 100644
--- a/.github/workflows/test-and-lint.yml
+++ b/.github/workflows/test-and-lint.yml
@@ -6,9 +6,15 @@ jobs:
   test:
     runs-on: ${{ matrix.os }}
     strategy:
+      fail-fast: false
       matrix:
-        python-version: [3.7, 3.8, 3.9]
-        os: [ubuntu-latest, windows-latest, macOS-latest]
+        python-version: [3.8, 3.9]
+        os: [
+          ubuntu-18.04,
+          ubuntu-20.04,
+          windows-latest,
+          macOS-10.15,
+        ]
 
     steps:
     - uses: actions/checkout@v1
@@ -20,13 +26,17 @@ jobs:
       run: |
         python -m pip install --upgrade pip
         pip install .[test]
+    - name: Download Test Resources
+      run: |
+        python scripts/download_test_resources.py
     - name: Test with pytest
       run: |
-        pytest napari_aicsimageio/tests/
+        pytest --cov-report xml --cov=napari_aicsimageio napari_aicsimageio/tests/
+    - name: Upload codecov
+      uses: codecov/codecov-action@v1
 
   lint:
     runs-on: ubuntu-latest
-
     steps:
     - uses: actions/checkout@v1
     - name: Set up Python
@@ -40,6 +50,6 @@ jobs:
     - name: Lint with flake8
       run: |
         flake8 napari_aicsimageio --count --verbose --show-source --statistics
-    - name: Check with black
-      run: |
-        black --check napari_aicsimageio
+        isort napari_aicsimageio --check-only
+        mypy napari_aicsimageio
+        black --check --exclude vendor napari_aicsimageio
\ No newline at end of file
diff --git a/napari_aicsimageio/tests/conftest.py b/napari_aicsimageio/tests/conftest.py
index 22a454a..a359b22 100644
--- a/napari_aicsimageio/tests/conftest.py
+++ b/napari_aicsimageio/tests/conftest.py
@@ -7,5 +7,5 @@
 
 
 @pytest.fixture
-def data_dir() -> Path:
-    return Path(__file__).parent / "data"
+def resources_dir() -> Path:
+    return Path(__file__).parent / "resources"
diff --git a/napari_aicsimageio/tests/data/example.gif b/napari_aicsimageio/tests/data/example.gif
deleted file mode 100644
index adae2ca..0000000
Binary files a/napari_aicsimageio/tests/data/example.gif and /dev/null differ
diff --git a/napari_aicsimageio/tests/data/example.png b/napari_aicsimageio/tests/data/example.png
deleted file mode 100644
index 04e55cb..0000000
Binary files a/napari_aicsimageio/tests/data/example.png and /dev/null differ
diff --git a/napari_aicsimageio/tests/data/s_1_t_10_c_3_z_1.tiff b/napari_aicsimageio/tests/data/s_1_t_10_c_3_z_1.tiff
deleted file mode 100755
index c81fe5c..0000000
Binary files a/napari_aicsimageio/tests/data/s_1_t_10_c_3_z_1.tiff and /dev/null differ
diff --git a/napari_aicsimageio/tests/data/s_1_t_1_c_10_z_1.ome.tiff b/napari_aicsimageio/tests/data/s_1_t_1_c_10_z_1.ome.tiff
deleted file mode 100755
index f7a36c4..0000000
Binary files a/napari_aicsimageio/tests/data/s_1_t_1_c_10_z_1.ome.tiff and /dev/null differ
diff --git a/napari_aicsimageio/tests/data/s_1_t_1_c_1_z_1.czi b/napari_aicsimageio/tests/data/s_1_t_1_c_1_z_1.czi
deleted file mode 100755
index 0bd1f1b..0000000
Binary files a/napari_aicsimageio/tests/data/s_1_t_1_c_1_z_1.czi and /dev/null differ
diff --git a/napari_aicsimageio/tests/data/s_1_t_1_c_1_z_1.ome.tiff b/napari_aicsimageio/tests/data/s_1_t_1_c_1_z_1.ome.tiff
deleted file mode 100755
index 5db33ed..0000000
Binary files a/napari_aicsimageio/tests/data/s_1_t_1_c_1_z_1.ome.tiff and /dev/null differ
diff --git a/napari_aicsimageio/tests/data/s_1_t_1_c_1_z_1.tiff b/napari_aicsimageio/tests/data/s_1_t_1_c_1_z_1.tiff
deleted file mode 100755
index 55dd9dd..0000000
Binary files a/napari_aicsimageio/tests/data/s_1_t_1_c_1_z_1.tiff and /dev/null differ
diff --git a/napari_aicsimageio/tests/data/s_3_t_1_c_3_z_5.czi b/napari_aicsimageio/tests/data/s_3_t_1_c_3_z_5.czi
deleted file mode 100755
index 278ab93..0000000
Binary files a/napari_aicsimageio/tests/data/s_3_t_1_c_3_z_5.czi and /dev/null differ
diff --git a/napari_aicsimageio/tests/data/s_3_t_1_c_3_z_5.ome.tiff b/napari_aicsimageio/tests/data/s_3_t_1_c_3_z_5.ome.tiff
deleted file mode 100755
index 63c5e55..0000000
Binary files a/napari_aicsimageio/tests/data/s_3_t_1_c_3_z_5.ome.tiff and /dev/null differ
diff --git a/napari_aicsimageio/tests/test_core.py b/napari_aicsimageio/tests/test_core.py
index e175265..5f2f105 100644
--- a/napari_aicsimageio/tests/test_core.py
+++ b/napari_aicsimageio/tests/test_core.py
@@ -1,49 +1,66 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
+from pathlib import Path
+from typing import Any, Dict, Tuple
+
 import dask.array as da
 import numpy as np
 import pytest
 
 from napari_aicsimageio import core
 
-# Example files
+###############################################################################
+
 PNG_FILE = "example.png"
 GIF_FILE = "example.gif"
-TIF_FILE = "s_1_t_1_c_1_z_1.tiff"
-CZI_FILE = "s_1_t_1_c_1_z_1.czi"
-OME_FILE = "s_1_t_1_c_1_z_1.ome.tiff"
-MED_TIF_FILE = "s_1_t_10_c_3_z_1.tiff"
-BIG_OME_FILE = "s_3_t_1_c_3_z_5.ome.tiff"
-BIG_CZI_FILE = "s_3_t_1_c_3_z_5.czi"
+OME_TIFF = "pipeline-4.ome.tiff"
+LIF_FILE = "tiled.lif"
+CZI_FILE = "variable_scene_shape_first_scene_pyramid.czi"
 
+###############################################################################
 
+
+@pytest.mark.parametrize(
+    "in_memory, expected_dtype",
+    [
+        (True, np.ndarray),
+        (False, da.core.Array),
+    ],
+)
 @pytest.mark.parametrize(
-    "filename, in_memory, expected_dtype, expected_shape, expected_channel_axis",
+    "filename, expected_shape, expected_meta",
     [
-        (PNG_FILE, True, np.ndarray, (1, 800, 537, 4), None),
-        (GIF_FILE, True, np.ndarray, (1, 72, 268, 268, 4), None),
-        (CZI_FILE, True, np.ndarray, (1, 1, 1, 325, 475), None),
-        (CZI_FILE, False, da.core.Array, (1, 1, 1, 325, 475), None),
-        (OME_FILE, True, np.ndarray, (1, 325, 475), None),
-        (OME_FILE, False, da.core.Array, (1, 325, 475), None),
-        (TIF_FILE, True, np.ndarray, (1, 325, 475), None),
-        (TIF_FILE, False, da.core.Array, (1, 325, 475), None),
-        ([CZI_FILE, CZI_FILE], True, np.ndarray, (2, 1, 1, 325, 475), None),
-        ([CZI_FILE, CZI_FILE], False, da.core.Array, (2, 1, 1, 325, 475), None),
-        (MED_TIF_FILE, False, da.core.Array, (1, 10, 3, 325, 475), None),
-        (BIG_CZI_FILE, False, da.core.Array, (1, 1, 3, 3, 5, 325, 475), None),
-        (BIG_OME_FILE, False, da.core.Array, (1, 3, 5, 3, 325, 475), None),
+        (PNG_FILE, (800, 537, 4), {"name": "Image:0", "rgb": True}),
+        (GIF_FILE, (72, 268, 268, 4), {"name": "Image:0", "rgb": True}),
+        (
+            CZI_FILE,
+            (3, 6183, 7705),
+            {"name": ["EGFP", "mCher", "PGC"], "channel_axis": 0},
+        ),
+        (
+            OME_TIFF,
+            (1, 4, 65, 600, 900),
+            {"name": ["Bright_2", "EGFP", "CMDRP", "H3342"], "channel_axis": 1},
+        ),
+        (
+            LIF_FILE,
+            (1, 4, 1, 5622, 7666),
+            {"name": ["Gray", "Red", "Green", "Cyan"], "channel_axis": 1},
+        ),
     ],
 )
 def test_reader(
-    data_dir, filename, in_memory, expected_dtype, expected_shape, expected_channel_axis
-):
-    # Append filename(s) to resources dir
+    resources_dir: Path,
+    filename: str,
+    in_memory: bool,
+    expected_dtype: type,
+    expected_shape: Tuple[int, ...],
+    expected_meta: Dict[str, Any],
+) -> None:
+    # Resolve filename to filepath
     if isinstance(filename, str):
-        path = str(data_dir / filename)
-    else:
-        path = [str(data_dir / _path) for _path in filename]
+        path = str(resources_dir / filename)
 
     # Get reader
     reader = core.get_reader(path, in_memory)
@@ -55,8 +72,13 @@ def test_reader(
     layer_data = reader(path)
 
     # We only return one layer
-    data, _, _ = layer_data[0]
+    if layer_data is not None:
+        data, meta, _ = layer_data[0]  # type: ignore
+
+        # Check layer data
+        assert isinstance(data, expected_dtype)  # type: ignore
+        assert data.shape == expected_shape  # type: ignore
 
-    # Check layer data
-    assert isinstance(data, expected_dtype)
-    assert data.shape == expected_shape
+        # Check meta
+        meta.pop("metadata", None)
+        assert meta == expected_meta  # type: ignore
diff --git a/scripts/download_test_resources.py b/scripts/download_test_resources.py
new file mode 100644
index 0000000..9d7d569
--- /dev/null
+++ b/scripts/download_test_resources.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import argparse
+import logging
+import sys
+import traceback
+from pathlib import Path
+
+from quilt3 import Package
+
+###############################################################################
+
+logging.basicConfig(
+    level=logging.INFO,
+    format="[%(levelname)4s: %(module)s:%(lineno)4s %(asctime)s] %(message)s",
+)
+log = logging.getLogger(__name__)
+
+###############################################################################
+# Args
+
+
+class Args(argparse.Namespace):
+    def __init__(self):
+        self.__parse()
+
+    def __parse(self):
+        # Setup parser
+        p = argparse.ArgumentParser(
+            prog="download_test_resources",
+            description=(
+                "Download files used for testing this project. This will download "
+                "all the required test resources and place them in the "
+                "`tests/resources` directory."
+            ),
+        )
+
+        # Arguments
+        p.add_argument(
+            "--top-hash",
+            # Generated package hash from upload_test_resources
+            default=None,
+            help=(
+                "A specific version of the package to retrieve. "
+                "If none, will read from the TEST_RESOURCES_HASH.txt file."
+            ),
+        )
+        p.add_argument(
+            "--debug",
+            action="store_true",
+            help="Show traceback if the script were to fail.",
+        )
+
+        # Parse
+        p.parse_args(namespace=self)
+
+
+###############################################################################
+# Build package
+
+
+def download_test_resources(args: Args):
+    # Try running the download pipeline
+    try:
+        # Get test resources dir
+        resources_dir = (
+            Path(__file__).parent.parent / "napari_aicsimageio" / "tests" / "resources"
+        ).resolve()
+        resources_dir.mkdir(exist_ok=True)
+
+        # Use or read top hash
+        if args.top_hash is None:
+            with open(Path(__file__).parent / "TEST_RESOURCES_HASH.txt", "r") as f:
+                top_hash = f.readline().rstrip()
+        else:
+            top_hash = args.top_hash
+
+        log.info(f"Downloading test resources using top hash: {top_hash}")
+
+        # Get quilt package
+        package = Package.browse(
+            "aicsimageio/test_resources",
+            "s3://aics-modeling-packages-test-resources",
+            top_hash=top_hash,
+        )
+
+        # Download
+        package["resources"].fetch(resources_dir)
+
+        log.info(f"Completed package download.")
+
+    # Catch any exception
+    except Exception as e:
+        log.error("=============================================")
+        if args.debug:
+            log.error("\n\n" + traceback.format_exc())
+            log.error("=============================================")
+        log.error("\n\n" + str(e) + "\n")
+        log.error("=============================================")
+        sys.exit(1)
+
+
+###############################################################################
+# Runner
+
+
+def main():
+    args = Args()
+    download_test_resources(args)
+
+
+###############################################################################
+# Allow caller to directly run this module (usually in development scenarios)
+
+if __name__ == "__main__":
+    main()