Skip to content

Commit

Permalink
Update style and fill out well-formed docs
Browse files Browse the repository at this point in the history
  • Loading branch information
joshmoore committed Aug 31, 2020
1 parent 77e631a commit 7a4f865
Show file tree
Hide file tree
Showing 18 changed files with 251 additions and 113 deletions.
4 changes: 2 additions & 2 deletions .bumpversion.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ commit = True
tag = True
sign_tags = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\.(?P<release>[a-z]+)(?P<build>\d+))?
serialize =
serialize =
{major}.{minor}.{patch}.{release}{build}
{major}.{minor}.{patch}

[bumpversion:part:release]
optional_value = prod
first_value = dev
values =
values =
dev
prod

Expand Down
38 changes: 36 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,48 @@ repos:
- id: black
args: [--target-version=py36]

- repo: https://github.com/asottile/pyupgrade
rev: v2.7.2
hooks:
- id: pyupgrade
args:
- --py36-plus

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v1.2.3
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-json
files: \.(json)$
- id: check-yaml
- id: fix-encoding-pragma
args:
- --remove
- id: trailing-whitespace
- id: check-case-conflict
- id: check-merge-conflict
- id: check-symlinks
- id: pretty-format-json
args:
- --autofix

- repo: https://gitlab.com/pycqa/flake8
rev: 3.8.3
hooks:
- id: flake8
additional_dependencies: [
flake8-blind-except,
flake8-builtins,
flake8-rst-docstrings,
flake8-logging-format,
]
args: [
# default black line length is 88
--max-line-length=88,
"--max-line-length=88",
# Conflicts with black: E203 whitespace before ':'
--ignore=E203,
"--ignore=E203",
"--rst-roles=class,func,ref,module,const",
]

- repo: https://github.com/pre-commit/mirrors-mypy
Expand All @@ -48,3 +81,4 @@ repos:
hooks:
- id: yamllint
# args: [--config-data=relaxed]
#
17 changes: 14 additions & 3 deletions ome_zarr/cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env python

"""Entrypoint for the `ome_zarr` command-line tool."""
import argparse
import logging
import sys
Expand All @@ -12,23 +11,34 @@


def config_logging(loglevel: int, args: argparse.Namespace) -> None:
"""Configure logging taking the `verbose` and `quiet` arguments into account.
Each `-v` increases the `loglevel` by 10 and each `-q` reduces the loglevel by 10.
For example, an initial loglevel of `INFO` will be converted to `DEBUG` via `-qqv`.
"""
loglevel = loglevel - (10 * args.verbose) + (10 * args.quiet)
logging.basicConfig(level=loglevel)
# DEBUG logging for s3fs so we can track remote calls
logging.getLogger("s3fs").setLevel(logging.DEBUG)


def info(args: argparse.Namespace) -> None:
"""Wrap the :func:`~ome_zarr.utils.info` method."""
config_logging(logging.WARN, args)
list(zarr_info(args.path))


def download(args: argparse.Namespace) -> None:
"""Wrap the :func:`~ome_zarr.utils.download` method."""
config_logging(logging.WARN, args)
zarr_download(args.path, args.output)


def create(args: argparse.Namespace) -> None:
"""Chooses between data generation methods in :module:`ome_zarr.utils` like.
:func:`~ome_zarr.data.coins` or :func:`~ome_zarr.data.astronaut`.
"""
config_logging(logging.WARN, args)
if args.method == "coins":
method = coins
Expand All @@ -42,6 +52,7 @@ def create(args: argparse.Namespace) -> None:


def scale(args: argparse.Namespace) -> None:
"""Wrap the :func:`~ome_zarr.scale.Scaler.scale` method."""
scaler = Scaler(
copy_metadata=args.copy_metadata,
downscale=args.downscale,
Expand All @@ -54,7 +65,7 @@ def scale(args: argparse.Namespace) -> None:


def main(args: List[str] = None) -> None:

"""Run appropriate function with argparse arguments, handling errors."""
parser = argparse.ArgumentParser()
parser.add_argument(
"-v",
Expand Down
26 changes: 25 additions & 1 deletion ome_zarr/conversions.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,34 @@
"""Simple conversion helpers."""

from typing import List


def int_to_rgba(v: int) -> List[float]:
"""Get rgba (0-1) e.g. (1, 0.5, 0, 1) from integer"""
"""Get rgba (0-1) e.g. (1, 0.5, 0, 1) from integer.
>>> print(int_to_rgba(0))
[0.0, 0.0, 0.0, 0.0]
>>> print([round(x, 3) for x in int_to_rgba(100100)])
[0.0, 0.004, 0.529, 0.016]
"""
return [x / 255 for x in v.to_bytes(4, signed=True, byteorder="big")]


def int_to_rgba_255(v: int) -> List[int]:
"""Get rgba (0-255) from integer.
>>> print(int_to_rgba_255(0))
[0, 0, 0, 0]
>>> print([round(x, 3) for x in int_to_rgba_255(100100)])
[0, 1, 135, 4]
"""
return [x for x in v.to_bytes(4, signed=True, byteorder="big")]


def rgba_to_int(r: int, g: int, b: int, a: int) -> int:
"""Use int.from_bytes to convert a color tuple.
>>> print(rgba_to_int(0, 0, 0, 0))
0
>>> print(rgba_to_int(0, 1, 135, 4))
100100
"""
return int.from_bytes([r, g, b, a], byteorder="big", signed=True)
27 changes: 21 additions & 6 deletions ome_zarr/data.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env python
"""Functions for generating synthetic data."""
from typing import Callable, List, Tuple

import numpy as np
Expand All @@ -17,9 +17,7 @@


def coins() -> Tuple[List, List]:
"""
Sample data from skimage
"""
"""Sample data from skimage."""
# Thanks to Juan
# https://gist.github.com/jni/62e07ddd135dbb107278bc04c0f9a8e7
image = data.coins()[50:-50, 50:-50]
Expand All @@ -37,6 +35,7 @@ def coins() -> Tuple[List, List]:


def astronaut() -> Tuple[List, List]:
"""Sample data from skimage."""
scaler = Scaler()

pixels = rgb_to_5d(np.tile(data.astronaut(), (2, 2, 1)))
Expand All @@ -53,6 +52,21 @@ def astronaut() -> Tuple[List, List]:


def make_circle(h: int, w: int, value: int, target: np.ndarray) -> None:
"""Apply a 2D circular mask to the given array.
>>> import numpy as np
>>> example = np.zeros((8, 8))
>>> make_circle(8, 8, 1, example)
>>> print(example)
[[0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 1. 1. 1. 1. 1. 0.]
[0. 1. 1. 1. 1. 1. 1. 1.]
[0. 1. 1. 1. 1. 1. 1. 1.]
[0. 1. 1. 1. 1. 1. 1. 1.]
[0. 1. 1. 1. 1. 1. 1. 1.]
[0. 1. 1. 1. 1. 1. 1. 1.]
[0. 0. 1. 1. 1. 1. 1. 0.]]
"""
x = np.arange(0, w)
y = np.arange(0, h)

Expand All @@ -65,7 +79,7 @@ def make_circle(h: int, w: int, value: int, target: np.ndarray) -> None:


def rgb_to_5d(pixels: np.ndarray) -> List:
"""convert an RGB image into 5D image (t, c, z, y, x)"""
"""Convert an RGB image into 5D image (t, c, z, y, x)."""
if len(pixels.shape) == 2:
stack = np.array([pixels])
channels = np.array([stack])
Expand All @@ -79,6 +93,7 @@ def rgb_to_5d(pixels: np.ndarray) -> List:


def write_multiscale(pyramid: List, group: zarr.Group) -> None:
"""Write a pyramid with multiscale metadata to disk."""
paths = []
for path, dataset in enumerate(pyramid):
group.create_dataset(str(path), data=pyramid[path])
Expand All @@ -93,7 +108,7 @@ def create_zarr(
method: Callable[..., Tuple[List, List]] = coins,
label_name: str = "coins",
) -> None:

"""Generate a synthetic image pyramid with labels."""
pyramid, labels = method()

store = zarr.DirectoryStore(zarr_directory)
Expand Down
53 changes: 44 additions & 9 deletions ome_zarr/io.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""
Reading logic for ome-zarr
"""Reading logic for ome-zarr.
Primary entry point is the :func:`~ome_zarr.io.parse_url` method.
"""

import json
Expand All @@ -19,17 +20,28 @@


class BaseZarrLocation(ABC):
"""
Base IO primitive for reading Zarr data.
No assumptions about the existence of the given path string are made.
Attempts are made to load various metadata files and cache them internally.
"""

def __init__(self, path: str) -> None:
self.zarr_path: str = path.endswith("/") and path or f"{path}/"
self.zarray: JSONDict = self.get_json(".zarray")
self.zgroup: JSONDict = self.get_json(".zgroup")
self.__metadata: JSONDict = {}
self.__exists: bool = True
if self.zgroup:
self.__metadata = self.get_json(".zattrs")
elif self.zarray:
self.__metadata = self.get_json(".zattrs")
else:
self.__exists = False

def __repr__(self) -> str:
"""Print the path as well as whether this is a group or an array."""
suffix = ""
if self.zgroup:
suffix += " [zgroup]"
Expand All @@ -38,47 +50,69 @@ def __repr__(self) -> str:
return f"{self.zarr_path}{suffix}"

def exists(self) -> bool:
return os.path.exists(self.zarr_path)
"""Return true if zgroup or zarray metadata exists."""
return self.__exists

def is_zarr(self) -> Optional[JSONDict]:
"""Return true if either zarray or zgroup metadata exists."""
return self.zarray or self.zgroup

@property
def root_attrs(self) -> JSONDict:
"""Return the contents of the zattrs file."""
return dict(self.__metadata)

@abstractmethod
def get_json(self, subpath: str) -> JSONDict:
"""Must be implemented by subclasses."""
raise NotImplementedError("unknown")

def load(self, subpath: str) -> da.core.Array:
"""
Use dask.array.from_zarr to load the subpath
"""
"""Use dask.array.from_zarr to load the subpath."""
return da.from_zarr(f"{self.zarr_path}{subpath}")

# TODO: update to from __future__ import annotations with 3.7+
def open(self, path: str) -> "BaseZarrLocation":
"""Create a new zarr for the given path"""
def create(self, path: str) -> "BaseZarrLocation":
"""Create a new Zarr location for the given path."""
subpath = posixpath.join(self.zarr_path, path)
subpath = posixpath.normpath(subpath)
LOGGER.debug(f"open({self.__class__.__name__}({subpath}))")
return self.__class__(posixpath.normpath(f"{subpath}"))


class LocalZarrLocation(BaseZarrLocation):
"""
Uses the :module:`json` library for loading JSON from disk.
"""

def get_json(self, subpath: str) -> JSONDict:
"""
Load and return a given subpath of self.zarr_path as JSON.
If a file does not exist, an empty response is returned rather
than an exception.
"""
filename = os.path.join(self.zarr_path, subpath)

if not os.path.exists(filename):
LOGGER.debug(f"{filename} does not exist")
return {}

with open(filename) as f:
return json.loads(f.read())


class RemoteZarrLocation(BaseZarrLocation):
""" Uses the :module:`requests` library for accessing Zarr metadata files. """

def get_json(self, subpath: str) -> JSONDict:
"""
Load and return a given subpath of self.zarr_path as JSON.
HTTP 403 and 404 responses are treated as if the file does not exist.
Exceptions during the remote connection are logged at the WARN level.
All other exceptions log at the ERROR level.
"""
url = f"{self.zarr_path}{subpath}"
try:
rsp = requests.get(url)
Expand All @@ -96,7 +130,8 @@ def get_json(self, subpath: str) -> JSONDict:


def parse_url(path: str) -> Optional[BaseZarrLocation]:
""" convert a path string or URL to a BaseZarrLocation instance
"""Convert a path string or URL to a BaseZarrLocation subclass.
>>> parse_url('does-not-exist')
"""
# Check is path is local directory first
Expand Down
9 changes: 3 additions & 6 deletions ome_zarr/napari.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
"""
This module is a napari plugin.
"""This module is a napari plugin.
It implements the ``napari_get_reader`` hook specification, (to create
a reader plugin).
It implements the ``napari_get_reader`` hook specification, (to create a reader plugin).
"""


Expand All @@ -29,8 +27,7 @@ def napari_hook_implementation(

@napari_hook_implementation
def napari_get_reader(path: PathLike) -> Optional[ReaderFunction]:
"""
Returns a reader for supported paths that include IDR ID
"""Returns a reader for supported paths that include IDR ID.
- URL of the form: https://s3.embassy.ebi.ac.uk/idr/zarr/v0.1/ID.zarr/
"""
Expand Down
Loading

0 comments on commit 7a4f865

Please sign in to comment.