Repository: imglib/imglyb (language: python)

Issue:
Align Package Structure with PyPA Recommendations
While there is generally no consensus on how python projects should be packaged, there seem to be some good benefits to the [`src` package structure](https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure) and the Python Packaging Authority now [promotes](https://packaging.python.org/en/latest/tutorials/packaging-projects/#a-simple-project) this structure.

https://github.com/pypa/packaging.python.org/issues/320 describes how they came to the determination they did.

We should consider adding it to imglyb.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..e6200ff
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,85 @@
+name: build
+
+on:
+  push:
+    branches:
+      - master
+    tags:
+      - "*-[0-9]+.*"
+  pull_request:
+    branches:
+      - master
+
+jobs:
+  build-cross-platform:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-latest, windows-latest, macos-latest]
+        python-version: ["3.7", "3.10"]
+        # Breaks due to https://github.com/jpype-project/jpype/issues/1009
+        # Should be included once the fix is released
+        exclude:
+          - os: windows-latest
+            python-version: "3.10"
+        include:
+          - os: windows-latest
+            python-version: "3.9"
+
+    steps:
+    - uses: actions/checkout@v2
+
+    - uses: actions/setup-python@v3
+      with:
+        python-version: ${{matrix.python-version}}
+
+    - name: Setup Java
+      uses: actions/setup-java@v3
+      with:
+        java-version: '8'
+        distribution: 'zulu'
+
+    - name: Install pyimagej
+      run: |
+        python -m pip install --upgrade pip
+        python -m pip install -e ".[dev]"
+
+    - name: Test ImageJ
+      run: |
+        python -m pytest -s -p no:faulthandler --color=yes
+        echo "Done!"
+
+  lint:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: psf/black@stable
+
+  conda-dev-test:
+    name: Test Conda Development Setup And Code Coverage
+    runs-on: ubuntu-latest
+    defaults:
+      # Steps that rely on the activated environment must be run with this shell setup.
+      # See https://github.com/marketplace/actions/setup-miniconda#important
+      run:
+        shell: bash -l {0}
+    steps:
+      - uses: actions/checkout@v2
+      - uses: conda-incubator/setup-miniconda@v2
+        with:
+          # Create env with dev packages
+          auto-update-conda: true
+          python-version: 3.9
+          environment-file: dev-environment.yml
+          # Activate imglyb-dev environment
+          activate-environment: imglyb-dev
+          auto-activate-base: false
+          # Use mamba for faster setup
+          use-mamba: true
+          mamba-version: "*"
+      - name: Test imglyb
+        run: |
+          python -m pytest test/ -p no:faulthandler --cov-report=xml --cov=.
+      # We could do this in its own action, but we'd have to setup the environment again.
+      - name: Upload Coverage to Codecov
+        uses: codecov/codecov-action@v2
diff --git a/.gitignore b/.gitignore
index 2c562d9..6d5ac0b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,11 @@ build
 dist
 *.pyc
 imglyb.egg-info
+
+# setuptools_scm
+
+src/imglyb/_version.py
+
+# VSCode
+
+.vscode
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..e69de29
diff --git a/dev-environment.yml b/dev-environment.yml
new file mode 100644
index 0000000..f45c5c2
--- /dev/null
+++ b/dev-environment.yml
@@ -0,0 +1,29 @@
+# Use this file to construct an environment
+# for developing Imglyb from source.
+#
+#   mamba env create -f dev-environment.yml
+#   conda activate imglyb-dev
+#
+# In addition to the dependencies needed for using Imglyb, it includes tools
+# for developer-related actions like running automated tests (pytest),
+# linting the code (black), and generating the API documentation (sphinx).
+# If you want an environment without these tools, use environment.yml.
+
+name: imglyb-dev
+channels:
+  - conda-forge
+  - defaults
+dependencies:
+  # Project dependencies
+  - numpy
+  - scyjava
+  # Developer tools
+  - black
+  - build
+  - pytest
+  - pytest-cov
+  - setuptools_scm >= 6.2
+  # Project from source
+  - pip
+  - pip:
+    - -e '.'
diff --git a/environment.yml b/environment.yml
index 14ee892..558758f 100644
--- a/environment.yml
+++ b/environment.yml
@@ -1,8 +1,23 @@
+# Use this file to construct an environment for working
+# with Imglyb a runtime environment:
+#
+#   mamba env create
+#   conda activate imglyb
+#
+# It includes the dependencies needed for using Imglyb, but not tools
+# for developer-related actions like running automated tests (pytest),
+# linting the code (black), and generating the API documentation (sphinx).
+# If you want an environment including these tools, use dev-environment.yml.
+
 name: imglyb
 channels:
   - conda-forge
   - defaults
 dependencies:
-  - imglyb
+  # Project dependencies
   - numpy
   - scyjava
+  # Project from source
+  - pip
+  - pip:
+    - -e .
diff --git a/examples/wrap_arraylike.py b/examples/wrap_arraylike.py
index 39456fa..d9e04ef 100644
--- a/examples/wrap_arraylike.py
+++ b/examples/wrap_arraylike.py
@@ -6,33 +6,33 @@
 
 Views = imglyb.util.Views
 
-shape      = (3, 5)
-data       = np.arange(np.prod(shape)).reshape(shape)
+shape = (3, 5)
+data = np.arange(np.prod(shape)).reshape(shape)
 block_size = (2, 2)
-img, _     = imglyb.as_cell_img(data, block_size, access_type='native', cache=1)
+img, _ = imglyb.as_cell_img(data, block_size, access_type="native", cache=1)
 
 print(data)
 print(img.toString())
 cursor = Views.flatIterable(img).cursor()
 
-print('Cell Img')
+print("Cell Img")
 while cursor.hasNext():
     print(cursor.next().toString())
 
 print()
-print('ndarray')
+print("ndarray")
 for d in data.flat:
     print(d)
 print(data)
 
 print()
-print('cells')
+print("cells")
 cursor = img.getCells().cursor()
 cellIdx = 0
 while cursor.hasNext():
     cell = cursor.next()
     access = cell.getData()
-    print(f'Cell index: {cellIdx} {access}')
+    print(f"Cell index: {cellIdx} {access}")
     size = cell.size()
     for idx in range(0, size):
         print(access.getValue(idx))
diff --git a/imglyb/__init__.py b/imglyb/__init__.py
deleted file mode 100644
index 5946495..0000000
--- a/imglyb/__init__.py
+++ /dev/null
@@ -1,25 +0,0 @@
-import logging
-import sys
-
-import scyjava
-import scyjava.config as sjconf
-
-from .config import get_imglib2_imglyb_version
-from .imglib_ndarray import ImgLibReferenceGuard as _ImgLibReferenceGuard
-from .ndarray_like_as_img import \
-    as_cell_img, \
-    as_cell_img_with_array_accesses, \
-    as_cell_img_with_native_accesses
-from .util import to_imglib, to_imglib_argb
-
-
-_logger = logging.getLogger(__name__)
-
-_imglib2_imglyb_version = get_imglib2_imglyb_version()
-_IMGLIB2_IMGLYB_ENDPOINT = 'net.imglib2:imglib2-imglyb:{}'.format(_imglib2_imglyb_version)
-sjconf.endpoints.append(_IMGLIB2_IMGLYB_ENDPOINT)
-
-
-def to_numpy(source):
-    scyjava.start_jvm()
-    return _ImgLibReferenceGuard(source)
diff --git a/imglyb/accesses.py b/imglyb/accesses.py
deleted file mode 100644
index 787fa1b..0000000
--- a/imglyb/accesses.py
+++ /dev/null
@@ -1,83 +0,0 @@
-import numpy as np
-import scyjava
-
-def _java_setup():
-    """
-    Lazy initialization function for Java-dependent data structures.
-    Do not call this directly; use scyjava.start_jvm() instead.
-    """
-
-    global Accesses
-    Accesses            = scyjava.jimport('net.imglib2.img.basictypeaccess.Accesses')
-
-    global ByteArray
-    ByteArray           = scyjava.jimport('net.imglib2.img.basictypeaccess.array.ByteArray')
-    global CharArray
-    CharArray           = scyjava.jimport('net.imglib2.img.basictypeaccess.array.CharArray')
-    global DoubleArray
-    DoubleArray         = scyjava.jimport('net.imglib2.img.basictypeaccess.array.DoubleArray')
-    global FloatArray
-    FloatArray          = scyjava.jimport('net.imglib2.img.basictypeaccess.array.FloatArray')
-    global IntArray
-    IntArray            = scyjava.jimport('net.imglib2.img.basictypeaccess.array.IntArray')
-    global LongArray
-    LongArray           = scyjava.jimport('net.imglib2.img.basictypeaccess.array.LongArray')
-    global ShortArray
-    ShortArray          = scyjava.jimport('net.imglib2.img.basictypeaccess.array.ShortArray')
-
-    global VolatileByteArray
-    VolatileByteArray   = scyjava.jimport('net.imglib2.img.basictypeaccess.volatiles.array.VolatileByteArray')
-    global VolatileCharArray
-    VolatileCharArray   = scyjava.jimport('net.imglib2.img.basictypeaccess.volatiles.array.VolatileCharArray')
-    global VolatileDoubleArray
-    VolatileDoubleArray = scyjava.jimport('net.imglib2.img.basictypeaccess.volatiles.array.VolatileDoubleArray')
-    global VolatileFloatArray
-    VolatileFloatArray  = scyjava.jimport('net.imglib2.img.basictypeaccess.volatiles.array.VolatileFloatArray')
-    global VolatileIntArray
-    VolatileIntArray    = scyjava.jimport('net.imglib2.img.basictypeaccess.volatiles.array.VolatileIntArray')
-    global VolatileLongArray
-    VolatileLongArray   = scyjava.jimport('net.imglib2.img.basictypeaccess.volatiles.array.VolatileLongArray')
-    global VolatileShortArray
-    VolatileShortArray  = scyjava.jimport('net.imglib2.img.basictypeaccess.volatiles.array.VolatileShortArray')
-
-    global ByteUnsafe
-    ByteUnsafe          = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.ByteUnsafe')
-    global CharUnsafe
-    CharUnsafe          = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.CharUnsafe')
-    global DoubleUnsafe
-    DoubleUnsafe        = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.DoubleUnsafe')
-    global FloatUnsafe
-    FloatUnsafe         = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.FloatUnsafe')
-    global IntUnsafe
-    IntUnsafe           = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.IntUnsafe')
-    global LongUnsafe
-    LongUnsafe          = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.LongUnsafe')
-    global ShortUnsafe
-    ShortUnsafe         = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.ShortUnsafe')
-
-scyjava.when_jvm_starts(_java_setup)
-
-
-# does not work with strided accesses, currently
-def as_array_access(ndarray, volatile=False):
-    scyjava.start_jvm()
-    if ndarray.dtype == np.uint8 or ndarray.dtype == np.int8:
-        return _as_array_access(ndarray, ByteUnsafe, lambda n : VolatileByteArray(n, True) if volatile else ByteArray(n))
-    elif ndarray.dtype == np.uint16 or ndarray.dtype == np.int16:
-        return _as_array_access(ndarray, ShortUnsafe, lambda n : VolatileShortArray(n, True) if volatile else ShortArray(n))
-    elif ndarray.dtype == np.uint32 or ndarray.dtype == np.int32:
-        return _as_array_access(ndarray, IntUnsafe, lambda n : VolatileIntArray(n, True) if volatile else IntArray(n))
-    elif ndarray.dtype == np.uint64 or ndarray.dtype == np.int64:
-        return _as_array_access(ndarray, LongUnsafe, lambda n : VolatileLongArray(n, True) if volatile else LongArray(n))
-    elif ndarray.dtype == np.float32:
-        return _as_array_access(ndarray, FloatUnsafe, lambda n : VolatileFloatArray(n, True) if volatile else FloatArray(n))
-    elif ndarray.dtype == np.float64:
-        return _as_array_access(ndarray, DoubleUnsafe, lambda n : VolatileDoubleArray(n, True) if volatile else DoubleArray(n))
-
-
-def _as_array_access(ndarray, src_clazz, tgt_clazz):
-    src = src_clazz(ndarray.ctypes.data)
-    tgt = tgt_clazz(ndarray.size)
-    Accesses.copyAny(src, 0, tgt, 0, ndarray.size)
-    return tgt
-
diff --git a/imglyb/caches.py b/imglyb/caches.py
deleted file mode 100644
index 62a1bd2..0000000
--- a/imglyb/caches.py
+++ /dev/null
@@ -1,17 +0,0 @@
-import scyjava
-
-def _java_setup():
-    """
-    Lazy initialization function for Java-dependent data structures.
-    Do not call this directly; use scyjava.start_jvm() instead.
-    """
-    global BoundedSoftRefLoaderCache
-    BoundedSoftRefLoaderCache   = scyjava.jimport('net.imglib2.cache.ref.BoundedSoftRefLoaderCache')
-    global GuardedStrongRefLoaderCache
-    GuardedStrongRefLoaderCache = scyjava.jimport('net.imglib2.cache.ref.GuardedStrongRefLoaderCache')
-    global SoftRefLoaderCache
-    SoftRefLoaderCache          = scyjava.jimport('net.imglib2.cache.ref.SoftRefLoaderCache')
-    global WeakRefLoaderCache
-    WeakRefLoaderCache          = scyjava.jimport('net.imglib2.cache.ref.WeakRefLoaderCache')
-
-scyjava.when_jvm_starts(_java_setup)
diff --git a/imglyb/config.py b/imglyb/config.py
deleted file mode 100644
index 2f256c7..0000000
--- a/imglyb/config.py
+++ /dev/null
@@ -1,44 +0,0 @@
-import os
-
-__major__   = 2
-__minor__   = 0
-__patch__   = 2
-__tag__     = 'dev0'
-__version__ = f'{__major__}.{__minor__}.{__patch__}.{__tag__}'.strip('.')
-
-class _Version(object):
-
-    def major(self):
-        return __major__
-
-    def minor(self):
-        return __minor__
-
-    def patch(self):
-        return __patch__
-
-    def tag(self):
-        return __tag__
-
-    def version(self):
-        return __version__
-
-    def __str__(self):
-        return self.version()
-
-_version = _Version()
-
-version = _version.version()
-
-_default_imglib2_imglyb_version = '1.0.1'
-_imglib2_imglyb_version         = os.getenv('IMGLIB2_IMGLYB_VERSION', _default_imglib2_imglyb_version)
-
-
-def set_imglib2_imglyb_version(version):
-    global _imglib2_imglyb_version
-    _imglib2_imglyb_version = version
-
-
-def get_imglib2_imglyb_version():
-    global _imglib2_imglyb_version
-    return _imglib2_imglyb_version
diff --git a/imglyb/types.py b/imglyb/types.py
deleted file mode 100644
index 433884b..0000000
--- a/imglyb/types.py
+++ /dev/null
@@ -1,83 +0,0 @@
-import numpy as np
-import scyjava
-
-def _java_setup():
-    """
-    Lazy initialization function for Java-dependent data structures.
-    Do not call this directly; use scyjava.start_jvm() instead.
-    """
-
-    global NativeType
-    NativeType                = scyjava.jimport('net.imglib2.type.NativeType')
-
-    global FloatType
-    FloatType                 = scyjava.jimport('net.imglib2.type.numeric.real.FloatType')
-    global DoubleType
-    DoubleType                = scyjava.jimport('net.imglib2.type.numeric.real.DoubleType')
-    global ByteType
-    ByteType                  = scyjava.jimport('net.imglib2.type.numeric.integer.ByteType')
-    global UnsignedByteType
-    UnsignedByteType          = scyjava.jimport('net.imglib2.type.numeric.integer.UnsignedByteType')
-    global ShortType
-    ShortType                 = scyjava.jimport('net.imglib2.type.numeric.integer.ShortType')
-    global UnsignedShortType
-    UnsignedShortType         = scyjava.jimport('net.imglib2.type.numeric.integer.UnsignedShortType')
-    global IntType
-    IntType                   = scyjava.jimport('net.imglib2.type.numeric.integer.IntType')
-    global UnsignedIntType
-    UnsignedIntType           = scyjava.jimport('net.imglib2.type.numeric.integer.UnsignedIntType')
-    global LongType
-    LongType                  = scyjava.jimport('net.imglib2.type.numeric.integer.LongType')
-    global UnsignedLongType
-    UnsignedLongType          = scyjava.jimport('net.imglib2.type.numeric.integer.UnsignedLongType')
-
-    global VolatileFloatType
-    VolatileFloatType         = scyjava.jimport('net.imglib2.type.volatiles.VolatileFloatType')
-    global VolatileDoubleType
-    VolatileDoubleType        = scyjava.jimport('net.imglib2.type.volatiles.VolatileDoubleType')
-    global VolatileByteType
-    VolatileByteType          = scyjava.jimport('net.imglib2.type.volatiles.VolatileByteType')
-    global VolatileUnsignedByteType
-    VolatileUnsignedByteType  = scyjava.jimport('net.imglib2.type.volatiles.VolatileUnsignedByteType')
-    global VolatileShortType
-    VolatileShortType         = scyjava.jimport('net.imglib2.type.volatiles.VolatileShortType')
-    global VolatileUnsignedShortType
-    VolatileUnsignedShortType = scyjava.jimport('net.imglib2.type.volatiles.VolatileUnsignedShortType')
-    global VolatileIntType
-    VolatileIntType           = scyjava.jimport('net.imglib2.type.volatiles.VolatileIntType')
-    global VolatileUnsignedIntType
-    VolatileUnsignedIntType   = scyjava.jimport('net.imglib2.type.volatiles.VolatileUnsignedIntType')
-    global VolatileLongType
-    VolatileLongType          = scyjava.jimport('net.imglib2.type.volatiles.VolatileLongType')
-    global VolatileUnsignedLongType
-    VolatileUnsignedLongType  = scyjava.jimport('net.imglib2.type.volatiles.VolatileUnsignedLongType')
-
-scyjava.when_jvm_starts(_java_setup)
-
-
-def for_np_dtype(dtype, volatile=False):
-    scyjava.start_jvm()
-    if dtype == np.uint8:
-        return VolatileUnsignedByteType() if volatile else UnsignedByteType()
-    if dtype == np.int8:
-        return VolatileByteType() if volatile else ByteType()
-
-    if dtype == np.uint16:
-        return VolatileUnsignedShortType() if volatile else UnsignedShortType()
-    if dtype == np.int16:
-        return VolatileShortType() if volatile else ShortType()
-
-    if dtype == np.uint32:
-        return VolatileUnsignedIntType() if volatile else UnsignedIntType()
-    if dtype == np.int32:
-        return VolatileIntType() if volatile else IntType()
-
-    if dtype == np.uint64:
-        return VolatileUnsignedLongType() if volatile else UnsignedLongType()
-    if dtype == np.int64:
-        return VolatileLongType() if volatile else LongType()
-
-    if dtype == np.float32:
-        return VolatileFloatType() if volatile else FloatType()
-    if dtype == np.float64:
-        return VolatileDoubleType() if volatile else DoubleType()
diff --git a/imglyb/util.py b/imglyb/util.py
deleted file mode 100644
index bd773f3..0000000
--- a/imglyb/util.py
+++ /dev/null
@@ -1,157 +0,0 @@
-import logging
-
-import numpy as np
-import scyjava
-
-from jpype import JImplements, JOverride, JLong, JArray
-
-_logger = logging.getLogger(__name__)
-
-__all__ = (
-    'to_imglib',
-    'to_imglib_argb'
-)
-
-def _java_setup():
-    """
-    Lazy initialization function for Java-dependent data structures.
-    Do not call this directly; use scyjava.start_jvm() instead.
-    """
-
-    # java
-    global Random
-    Random = scyjava.jimport('java.util.Random')
-
-    # imglib
-    try:
-        global Helpers
-        Helpers                            = scyjava.jimport('net.imglib2.python.Helpers')
-        global NumpyToImgLibConversions
-        NumpyToImgLibConversions           = scyjava.jimport('net.imglib2.python.NumpyToImgLibConversions')
-        global NumpyToImgLibConversionsWithStride
-        NumpyToImgLibConversionsWithStride = scyjava.jimport('net.imglib2.python.NumpyToImgLibConversionsWithStride')
-        global Views
-        Views                              = scyjava.jimport('net.imglib2.view.Views')
-    except TypeError as e:
-        _logger.error("Failed to import ImgLib2 helper classes. Please ensure imglib2-imglyb is present on the classpath.")
-        raise e
-
-    # Guard
-    global ReferenceGuardingRandomAccessibleInterval
-    ReferenceGuardingRandomAccessibleInterval = scyjava.jimport('net.imglib2.python.ReferenceGuardingRandomAccessibleInterval')
-
-    global numpy_dtype_to_conversion_method
-    numpy_dtype_to_conversion_method = {
-        np.dtype('complex64')  : NumpyToImgLibConversions.toComplexFloat,
-        np.dtype('complex128') : NumpyToImgLibConversions.toComplexDouble,
-        np.dtype('float32')    : NumpyToImgLibConversions.toFloat,
-        np.dtype('float64')    : NumpyToImgLibConversions.toDouble,
-        np.dtype('int8')       : NumpyToImgLibConversions.toByte,
-        np.dtype('int16')      : NumpyToImgLibConversions.toShort,
-        np.dtype('int32')      : NumpyToImgLibConversions.toInt,
-        np.dtype('int64')      : NumpyToImgLibConversions.toLong,
-        np.dtype('uint8')      : NumpyToImgLibConversions.toUnsignedByte,
-        np.dtype('uint16')     : NumpyToImgLibConversions.toUnsignedShort,
-        np.dtype('uint32')     : NumpyToImgLibConversions.toUnsignedInt,
-        np.dtype('uint64')     : NumpyToImgLibConversions.toUnsignedLong
-    }
-
-    global numpy_dtype_to_conversion_with_stride_method
-    numpy_dtype_to_conversion_with_stride_method = {
-        np.dtype('complex64')  : NumpyToImgLibConversionsWithStride.toComplexFloat,
-        np.dtype('complex128') : NumpyToImgLibConversionsWithStride.toComplexDouble,
-        np.dtype('float32')    : NumpyToImgLibConversionsWithStride.toFloat,
-        np.dtype('float64')    : NumpyToImgLibConversionsWithStride.toDouble,
-        np.dtype('int8')       : NumpyToImgLibConversionsWithStride.toByte,
-        np.dtype('int16')      : NumpyToImgLibConversionsWithStride.toShort,
-        np.dtype('int32')      : NumpyToImgLibConversionsWithStride.toInt,
-        np.dtype('int64')      : NumpyToImgLibConversionsWithStride.toLong,
-        np.dtype('uint8')      : NumpyToImgLibConversionsWithStride.toUnsignedByte,
-        np.dtype('uint16')     : NumpyToImgLibConversionsWithStride.toUnsignedShort,
-        np.dtype('uint32')     : NumpyToImgLibConversionsWithStride.toUnsignedInt,
-        np.dtype('uint64')     : NumpyToImgLibConversionsWithStride.toUnsignedLong
-    }
-
-    global ReferenceGuard
-    @JImplements('net.imglib2.python.ReferenceGuardingRandomAccessibleInterval$ReferenceHolder')
-    class ReferenceGuard():
-
-        def __init__(self, *args, **kwargs):
-            self.args = args
-
-    global GenericMouseMotionListener
-    @JImplements('java.awt.event.MouseMotionListener')
-    class GenericMouseMotionListener():
-
-        def __init__(self, mouse_dragged=lambda e: None, mouse_moved=lambda e: None):
-            self.mouse_dragged = mouse_dragged
-            self.mouse_moved = mouse_moved
-
-        @JOverride
-        def mouseDragged(self, e):
-            self.mouse_dragged(e)
-
-        @JOverride
-        def mouseMoved(self, e):
-            self.mouse_moved(e)
-
-
-    global RunnableFromFunc
-    @JImplements('java.lang.Runnable')
-    class RunnableFromFunc():
-
-        def __init__(self, func):
-            self.func = func
-
-        @JOverride
-        def run(self):
-            _logger.debug('Running function %s', self.func)
-            self.func()
-
-scyjava.when_jvm_starts(_java_setup)
-
-
-def _get_address(source):
-    return source.ctypes.data
-
-
-# how to use type hints for python < 3.5?
-def to_imglib(source):
-    scyjava.start_jvm()
-    return ReferenceGuardingRandomAccessibleInterval(_to_imglib(source), ReferenceGuard(source))
-
-
-# how to use type hints for python < 3.5?
-def _to_imglib(source):
-    address = _get_address(source)
-    long_address = JLong(address)
-    long_arr_source = JArray(JLong)(source.shape[::-1])
-
-    if not source.dtype in numpy_dtype_to_conversion_method:
-        raise NotImplementedError("Cannot convert dtype to ImgLib2 type yet: {}".format(source.dtype))
-    elif source.flags['CARRAY']:
-        return numpy_dtype_to_conversion_method[source.dtype](long_address, *long_arr_source)
-    else:
-        stride = np.array(source.strides[::-1]) / source.itemsize
-        long_arr_stride = JArray(JLong)(stride)
-        return numpy_dtype_to_conversion_with_stride_method[source.dtype](long_address, long_arr_stride, long_arr_source)
-
-
-def to_imglib_argb(source):
-    scyjava.start_jvm()
-    return ReferenceGuardingRandomAccessibleInterval(_to_imglib_argb(source), ReferenceGuard(source))
-
-
-def _to_imglib_argb(source):
-    address = _get_address(source)
-    long_address = JLong(address)
-    long_arr_source = JArray(JLong)(source.shape[::-1])
-
-    if not (source.dtype == np.dtype('int32') or source.dtype == np.dtype('uint32')):
-        raise NotImplementedError("source.dtype must be int32 or uint32")
-    if source.flags['CARRAY']:
-        return NumpyToImgLibConversions.toARGB(long_address, *long_arr_source)
-    else:
-        stride = np.array(source.strides[::-1]) / source.itemsize
-        long_arr_stride = JArray(JLong)(stride)
-        return NumpyToImgLibConversionsWithStride.toARGB(long_address, long_arr_stride, long_arr_source)
diff --git a/imglyb/version.py b/imglyb/version.py
deleted file mode 100644
index 21078f4..0000000
--- a/imglyb/version.py
+++ /dev/null
@@ -1,1 +0,0 @@
-from .config import version as __version__
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..ed7c521
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,15 @@
+[build-system]
+# Setuptools version constraint ensures PEP 517 compatibility
+requires = [
+    "setuptools >= 45",
+    "wheel",
+    "setuptools_scm>=6.2",
+]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools_scm]
+write_to = "src/imglyb/version.py"
+
+[tool.black]
+# This file is autogenerated by setuptools_scm - it cannot be modified
+--exclude = "src/imglyb/version.py"
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index 12871ff..be0c050 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,2 +1,57 @@
 [metadata]
-description-file=README.md
+name = imglyb
+# version handled by setuptools_scm. See https://github.com/pypa/setuptools_scm/#default-versioning-scheme
+author = Philipp Hanslovsky, Curtis Rueden, Edward Evans, Mark Hiner, Gabriel Selzer
+author_email = ctrueden@wisc.edu
+description = A python module to bring together the worlds of numpy (Python) and ImgLib2 (Java).
+long_description = file: README.md
+long_description_content_type = text/markdown
+license = Simplified BSD License
+url = https://github.com/imglib/imglyb
+platforms = ['any']
+project_urls =
+    Bug Tracker = https://github.com/imglib/imglyb/issues
+    Documentation = https://github.com/imglib/imglyb/blob/master/README.md
+    Source Code = https://github.com/imglib/imglyb
+classifiers =
+    Development Status :: 5 - Production/Stable
+    Intended Audience :: Developers
+    Intended Audience :: Education
+    Intended Audience :: Science/Research
+    Programming Language :: Python :: 3 :: Only
+    Programming Language :: Python :: 3 :: 3.7
+    Programming Language :: Python :: 3 :: 3.8
+    Programming Language :: Python :: 3 :: 3.9
+    Programming Language :: Python :: 3 :: 3.10
+    License :: OSI Approved :: BSD License
+    Operating System :: Microsoft :: Windows
+    Operating System :: Unix
+    Operating System :: MacOS
+    Topic :: Scientific/Engineering
+    Topic :: Scientific/Engineering :: Image Processing
+    Topic :: Scientific/Engineering :: Visualization
+    Topic :: Software Development :: Libraries :: Java Libraries
+    Topic :: Software Development :: Libraries :: Python Modules
+    Topic :: Utilities
+
+[options]
+packages = find:
+package_dir =
+    = src
+
+# Ensure any changes made to this list are also added to environment.yml AND environment-test.yml!
+python_requires = >= 3.7
+install_requires =
+    numpy
+    jpype1
+    scyjava >= 1.3.0
+    setuptools_scm >= 6.2
+
+[options.extras_require]
+
+# Ensure any changes to this list are also added to environment-test.yml!
+dev =
+    build
+    black
+    pytest
+    pytest-cov
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 51dfea5..0000000
--- a/setup.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from setuptools import setup, find_packages
-from os import path
-
-here = path.abspath(path.dirname(__file__))
-
-with open(path.join(here, 'README.md')) as f:
-    imglyb_long_description = f.read()
-
-version_info={}
-with open('imglyb/config.py', 'r') as f:
-    exec(f.read(), version_info)
-version = version_info['version']
-
-setup(
-    name='imglyb',
-    python_requires='>=3.6',
-    packages=find_packages(),
-    version=version,
-    author='Philipp Hanslovsky, Curtis Rueden, Edward Evans, Mark Hiner',
-    author_email='ctrueden@wisc.edu',
-    description='A python module to bring together the worlds of numpy (Python) and ImgLib2 (Java).',
-    long_description=imglyb_long_description,
-    long_description_content_type='text/markdown',
-    license='Simplified BSD License',
-    url='https://github.com/imglib/imglyb',
-    install_requires=['numpy', 'jpype1', 'scyjava >= 1.3.0'],
-)
diff --git a/imglyb/OSXAWTwrapper.py b/src/imglyb/OSXAWTwrapper.py
similarity index 93%
rename from imglyb/OSXAWTwrapper.py
rename to src/imglyb/OSXAWTwrapper.py
index 0ab084f..e9983b9 100644
--- a/imglyb/OSXAWTwrapper.py
+++ b/src/imglyb/OSXAWTwrapper.py
@@ -20,6 +20,7 @@
 
 usage = "usage: python OSXAWTwrapper.py [module name | script path] [module or script parameters]"
 
+
 def runAwtStuff():
 
     import runpy
@@ -35,7 +36,7 @@ def runAwtStuff():
         try:
             if os.path.exists(name):
                 runpy.run_path(name, run_name="__main__")
-            else:            
+            else:
                 runpy.run_module(name, run_name="__main__")
         except Exception as e:
             print("exception occurred while running {}: {}".format(name, e))
@@ -63,10 +64,16 @@ def main():
     try:
         import objc
         from PyObjCTools import AppHelper
-        from AppKit import NSApplication, NSApp, NSObject, NSApplicationActivationPolicyRegular
+        from AppKit import (
+            NSApplication,
+            NSApp,
+            NSObject,
+            NSApplicationActivationPolicyRegular,
+        )
+
         # from Foundation import *
 
-        class AppDelegate (NSObject):
+        class AppDelegate(NSObject):
             def init(self):
                 self = objc.super(AppDelegate, self).init()
                 if self is None:
@@ -91,8 +98,8 @@ def applicationDidFinishLaunching_(self, aNotification):
         NSApp.setActivationPolicy_(NSApplicationActivationPolicyRegular)
         AppHelper.runEventLoop()
     except ModuleNotFoundError:
-        print("Skipping OSXAWTwrapper - module 'objc' is not installed") 
+        print("Skipping OSXAWTwrapper - module 'objc' is not installed")
 
-if __name__ == '__main__' : 
-    main()
 
+if __name__ == "__main__":
+    main()
diff --git a/src/imglyb/__init__.py b/src/imglyb/__init__.py
new file mode 100644
index 0000000..25ed87f
--- /dev/null
+++ b/src/imglyb/__init__.py
@@ -0,0 +1,96 @@
+from functools import lru_cache
+import logging
+import sys
+from typing import Any, Callable, Dict
+
+import scyjava
+import scyjava.config as sjconf
+import imglyb.version
+
+from .config import get_imglib2_imglyb_version
+from .imglib_ndarray import ImgLibReferenceGuard as _ImgLibReferenceGuard
+from .ndarray_like_as_img import (
+    as_cell_img,
+    as_cell_img_with_array_accesses,
+    as_cell_img_with_native_accesses,
+)
+from .util import to_imglib, to_imglib_argb
+
+
+_logger = logging.getLogger(__name__)
+
+_imglib2_imglyb_version = get_imglib2_imglyb_version()
+_IMGLIB2_IMGLYB_ENDPOINT = "net.imglib2:imglib2-imglyb:{}".format(
+    _imglib2_imglyb_version
+)
+sjconf.endpoints.append(_IMGLIB2_IMGLYB_ENDPOINT)
+
+
+def to_numpy(source):
+    scyjava.start_jvm()
+    return _ImgLibReferenceGuard(source)
+
+
+# This is a duplication of work in scyjava.
+# It should be removed once https://github.com/scijava/scyjava/issues/40 has been solved.
+
+# Set of module properties
+_CONSTANTS: Dict[str, Callable] = {}
+
+
+def constant(func: Callable[[], Any], cache=True) -> Callable[[], Any]:
+    """
+    Turns a function into a property of this module
+    Functions decorated with this property must have a
+    leading underscore!
+    :param func: The function to turn into a property
+    """
+    if func.__name__[0] != "_":
+        raise ValueError(
+            f"""Function {func.__name__} must have
+            a leading underscore in its name
+            to become a module property!"""
+        )
+    name = func.__name__[1:]
+    if cache:
+        func = (lru_cache(maxsize=None))(func)
+    _CONSTANTS[name] = func
+    return func
+
+
+def __getattr__(name):
+    """
+    Runs as a fallback when this module does not have an
+    attribute.
+    :param name: The name of the attribute being searched for.
+    """
+    if name in _CONSTANTS:
+        return _CONSTANTS[name]()
+    raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
+
+
+@constant
+def ___version__():
+    # First pass: use the version output by setuptools_scm
+    try:
+        import imglyb.version
+
+        return imglyb.version.version
+    except ImportError:
+        pass
+    # Second pass: use importlib.metadata
+    try:
+        from importlib.metadata import version, PackageNotFoundError
+
+        return version("imglyb")
+    except ImportError or PackageNotFoundError:
+        pass
+    # Third pass: use pkg_resources
+    try:
+        from pkg_resources import get_distribution
+
+        return get_distribution("imglyb").version
+    except ImportError:
+        pass
+    # Fourth pass: Give up
+    return "Cannot determine version! Ensure pkg_resources is installed!"
diff --git a/src/imglyb/accesses.py b/src/imglyb/accesses.py
new file mode 100644
index 0000000..8ee7be5
--- /dev/null
+++ b/src/imglyb/accesses.py
@@ -0,0 +1,134 @@
+import numpy as np
+import scyjava
+
+
+def _java_setup():
+    """
+    Lazy initialization function for Java-dependent data structures.
+    Do not call this directly; use scyjava.start_jvm() instead.
+    """
+
+    global Accesses
+    Accesses = scyjava.jimport("net.imglib2.img.basictypeaccess.Accesses")
+
+    global ByteArray
+    ByteArray = scyjava.jimport("net.imglib2.img.basictypeaccess.array.ByteArray")
+    global CharArray
+    CharArray = scyjava.jimport("net.imglib2.img.basictypeaccess.array.CharArray")
+    global DoubleArray
+    DoubleArray = scyjava.jimport("net.imglib2.img.basictypeaccess.array.DoubleArray")
+    global FloatArray
+    FloatArray = scyjava.jimport("net.imglib2.img.basictypeaccess.array.FloatArray")
+    global IntArray
+    IntArray = scyjava.jimport("net.imglib2.img.basictypeaccess.array.IntArray")
+    global LongArray
+    LongArray = scyjava.jimport("net.imglib2.img.basictypeaccess.array.LongArray")
+    global ShortArray
+    ShortArray = scyjava.jimport("net.imglib2.img.basictypeaccess.array.ShortArray")
+
+    global VolatileByteArray
+    VolatileByteArray = scyjava.jimport(
+        "net.imglib2.img.basictypeaccess.volatiles.array.VolatileByteArray"
+    )
+    global VolatileCharArray
+    VolatileCharArray = scyjava.jimport(
+        "net.imglib2.img.basictypeaccess.volatiles.array.VolatileCharArray"
+    )
+    global VolatileDoubleArray
+    VolatileDoubleArray = scyjava.jimport(
+        "net.imglib2.img.basictypeaccess.volatiles.array.VolatileDoubleArray"
+    )
+    global VolatileFloatArray
+    VolatileFloatArray = scyjava.jimport(
+        "net.imglib2.img.basictypeaccess.volatiles.array.VolatileFloatArray"
+    )
+    global VolatileIntArray
+    VolatileIntArray = scyjava.jimport(
+        "net.imglib2.img.basictypeaccess.volatiles.array.VolatileIntArray"
+    )
+    global VolatileLongArray
+    VolatileLongArray = scyjava.jimport(
+        "net.imglib2.img.basictypeaccess.volatiles.array.VolatileLongArray"
+    )
+    global VolatileShortArray
+    VolatileShortArray = scyjava.jimport(
+        "net.imglib2.img.basictypeaccess.volatiles.array.VolatileShortArray"
+    )
+
+    global ByteUnsafe
+    ByteUnsafe = scyjava.jimport(
+        "net.imglib2.img.basictypelongaccess.unsafe.ByteUnsafe"
+    )
+    global CharUnsafe
+    CharUnsafe = scyjava.jimport(
+        "net.imglib2.img.basictypelongaccess.unsafe.CharUnsafe"
+    )
+    global DoubleUnsafe
+    DoubleUnsafe = scyjava.jimport(
+        "net.imglib2.img.basictypelongaccess.unsafe.DoubleUnsafe"
+    )
+    global FloatUnsafe
+    FloatUnsafe = scyjava.jimport(
+        "net.imglib2.img.basictypelongaccess.unsafe.FloatUnsafe"
+    )
+    global IntUnsafe
+    IntUnsafe = scyjava.jimport("net.imglib2.img.basictypelongaccess.unsafe.IntUnsafe")
+    global LongUnsafe
+    LongUnsafe = scyjava.jimport(
+        "net.imglib2.img.basictypelongaccess.unsafe.LongUnsafe"
+    )
+    global ShortUnsafe
+    ShortUnsafe = scyjava.jimport(
+        "net.imglib2.img.basictypelongaccess.unsafe.ShortUnsafe"
+    )
+
+
+scyjava.when_jvm_starts(_java_setup)
+
+
+# does not work with strided accesses, currently
+def as_array_access(ndarray, volatile=False):
+    scyjava.start_jvm()
+    if ndarray.dtype == np.uint8 or ndarray.dtype == np.int8:
+        return _as_array_access(
+            ndarray,
+            ByteUnsafe,
+            lambda n: VolatileByteArray(n, True) if volatile else ByteArray(n),
+        )
+    elif ndarray.dtype == np.uint16 or ndarray.dtype == np.int16:
+        return _as_array_access(
+            ndarray,
+            ShortUnsafe,
+            lambda n: VolatileShortArray(n, True) if volatile else ShortArray(n),
+        )
+    elif ndarray.dtype == np.uint32 or ndarray.dtype == np.int32:
+        return _as_array_access(
+            ndarray,
+            IntUnsafe,
+            lambda n: VolatileIntArray(n, True) if volatile else IntArray(n),
+        )
+    elif ndarray.dtype == np.uint64 or ndarray.dtype == np.int64:
+        return _as_array_access(
+            ndarray,
+            LongUnsafe,
+            lambda n: VolatileLongArray(n, True) if volatile else LongArray(n),
+        )
+    elif ndarray.dtype == np.float32:
+        return _as_array_access(
+            ndarray,
+            FloatUnsafe,
+            lambda n: VolatileFloatArray(n, True) if volatile else FloatArray(n),
+        )
+    elif ndarray.dtype == np.float64:
+        return _as_array_access(
+            ndarray,
+            DoubleUnsafe,
+            lambda n: VolatileDoubleArray(n, True) if volatile else DoubleArray(n),
+        )
+
+
+def _as_array_access(ndarray, src_clazz, tgt_clazz):
+    src = src_clazz(ndarray.ctypes.data)
+    tgt = tgt_clazz(ndarray.size)
+    Accesses.copyAny(src, 0, tgt, 0, ndarray.size)
+    return tgt
diff --git a/src/imglyb/caches.py b/src/imglyb/caches.py
new file mode 100644
index 0000000..9da3558
--- /dev/null
+++ b/src/imglyb/caches.py
@@ -0,0 +1,23 @@
+import scyjava
+
+
+def _java_setup():
+    """
+    Lazy initialization function for Java-dependent data structures.
+    Do not call this directly; use scyjava.start_jvm() instead.
+    """
+    global BoundedSoftRefLoaderCache
+    BoundedSoftRefLoaderCache = scyjava.jimport(
+        "net.imglib2.cache.ref.BoundedSoftRefLoaderCache"
+    )
+    global GuardedStrongRefLoaderCache
+    GuardedStrongRefLoaderCache = scyjava.jimport(
+        "net.imglib2.cache.ref.GuardedStrongRefLoaderCache"
+    )
+    global SoftRefLoaderCache
+    SoftRefLoaderCache = scyjava.jimport("net.imglib2.cache.ref.SoftRefLoaderCache")
+    global WeakRefLoaderCache
+    WeakRefLoaderCache = scyjava.jimport("net.imglib2.cache.ref.WeakRefLoaderCache")
+
+
+scyjava.when_jvm_starts(_java_setup)
diff --git a/src/imglyb/config.py b/src/imglyb/config.py
new file mode 100644
index 0000000..6a6f09f
--- /dev/null
+++ b/src/imglyb/config.py
@@ -0,0 +1,19 @@
+import os
+from setuptools_scm import get_version
+
+version = get_version(root="../..", relative_to=__file__)
+
+_default_imglib2_imglyb_version = "1.0.1"
+_imglib2_imglyb_version = os.getenv(
+    "IMGLIB2_IMGLYB_VERSION", _default_imglib2_imglyb_version
+)
+
+
+def set_imglib2_imglyb_version(version):
+    global _imglib2_imglyb_version
+    _imglib2_imglyb_version = version
+
+
+def get_imglib2_imglyb_version():
+    global _imglib2_imglyb_version
+    return _imglib2_imglyb_version
diff --git a/imglyb/imglib_ndarray.py b/src/imglyb/imglib_ndarray.py
similarity index 55%
rename from imglyb/imglib_ndarray.py
rename to src/imglyb/imglib_ndarray.py
index 4e742d4..2305a4a 100644
--- a/imglyb/imglib_ndarray.py
+++ b/src/imglyb/imglib_ndarray.py
@@ -7,60 +7,62 @@
 
 from imglyb import util
 
+
 def _java_setup():
     """
     Lazy initialization function for Java-dependent data structures.
     Do not call this directly; use scyjava.start_jvm() instead.
     """
     global Intervals
-    Intervals = scyjava.jimport('net.imglib2.util.Intervals')
+    Intervals = scyjava.jimport("net.imglib2.util.Intervals")
+
 
 scyjava.when_jvm_starts(_java_setup)
 
 dtype_selector = {
-    'FloatType': np.dtype('float32'),
-    'FloatLongAccessType': np.dtype('float32'),
-    'DoubleType': np.dtype('float64'),
-    'DoubleLongAccessType': np.dtype('float64'),
-    'ByteType': np.dtype('int8'),
-    'ByteLongAccessType': np.dtype('int8'),
-    'UnsignedByteType': np.dtype('uint8'),
-    'UnsignedByteLongAccessType': np.dtype('uint8'),
-    'ShortType': np.dtype('int16'),
-    'ShortLongAccessType': np.dtype('int16'),
-    'UnsignedShortType': np.dtype('uint16'),
-    'UnisgnedShortLongAccessType': np.dtype('uint16'),
-    'IntType': np.dtype('int32'),
-    'IntLongAccessType': np.dtype('int32'),
-    'UnsignedIntType': np.dtype('uint32'),
-    'UnsignedIntLongAccessType': np.dtype('uint32'),
-    'LongType': np.dtype('int64'),
-    'LongLongAccessType': np.dtype('int64'),
-    'UnsignedLongType': np.dtype('uint64'),
-    'UnsignedLongLongAccessType': np.dtype('uint64'),
+    "FloatType": np.dtype("float32"),
+    "FloatLongAccessType": np.dtype("float32"),
+    "DoubleType": np.dtype("float64"),
+    "DoubleLongAccessType": np.dtype("float64"),
+    "ByteType": np.dtype("int8"),
+    "ByteLongAccessType": np.dtype("int8"),
+    "UnsignedByteType": np.dtype("uint8"),
+    "UnsignedByteLongAccessType": np.dtype("uint8"),
+    "ShortType": np.dtype("int16"),
+    "ShortLongAccessType": np.dtype("int16"),
+    "UnsignedShortType": np.dtype("uint16"),
+    "UnisgnedShortLongAccessType": np.dtype("uint16"),
+    "IntType": np.dtype("int32"),
+    "IntLongAccessType": np.dtype("int32"),
+    "UnsignedIntType": np.dtype("uint32"),
+    "UnsignedIntLongAccessType": np.dtype("uint32"),
+    "LongType": np.dtype("int64"),
+    "LongLongAccessType": np.dtype("int64"),
+    "UnsignedLongType": np.dtype("uint64"),
+    "UnsignedLongLongAccessType": np.dtype("uint64"),
 }
 
 ctype_conversions_imglib = {
-    'FloatType': ctypes.c_float,
-    'FloatLongAccessType': ctypes.c_float,
-    'DoubleType': ctypes.c_double,
-    'DoubleLongAccessType': ctypes.c_double,
-    'ByteType': ctypes.c_int8,
-    'ByteLongAccessType': ctypes.c_int8,
-    'UnsignedByteType': ctypes.c_uint8,
-    'UnsignedByteLongAccessType': ctypes.c_uint8,
-    'ShortType': ctypes.c_int16,
-    'ShortLongAccessType': ctypes.c_int16,
-    'UnsignedShortType': ctypes.c_uint16,
-    'UnsignedShortLongAccessType': ctypes.c_uint16,
-    'IntType': ctypes.c_int32,
-    'IntLongAccessType': ctypes.c_int32,
-    'UnsignedIntType': ctypes.c_uint32,
-    'UnsignedIntLongAccessType': ctypes.c_uint32,
-    'LongType': ctypes.c_int64,
-    'LongLongAccessType': ctypes.c_int64,
-    'UnsignedLongType': ctypes.c_uint64,
-    'UnsignedLongLongAccessType': ctypes.c_uint64
+    "FloatType": ctypes.c_float,
+    "FloatLongAccessType": ctypes.c_float,
+    "DoubleType": ctypes.c_double,
+    "DoubleLongAccessType": ctypes.c_double,
+    "ByteType": ctypes.c_int8,
+    "ByteLongAccessType": ctypes.c_int8,
+    "UnsignedByteType": ctypes.c_uint8,
+    "UnsignedByteLongAccessType": ctypes.c_uint8,
+    "ShortType": ctypes.c_int16,
+    "ShortLongAccessType": ctypes.c_int16,
+    "UnsignedShortType": ctypes.c_uint16,
+    "UnsignedShortLongAccessType": ctypes.c_uint16,
+    "IntType": ctypes.c_int32,
+    "IntLongAccessType": ctypes.c_int32,
+    "UnsignedIntType": ctypes.c_uint32,
+    "UnsignedIntLongAccessType": ctypes.c_uint32,
+    "LongType": ctypes.c_int64,
+    "LongLongAccessType": ctypes.c_int64,
+    "UnsignedLongType": ctypes.c_uint64,
+    "UnsignedLongLongAccessType": ctypes.c_uint64,
 }
 
 
@@ -68,18 +70,20 @@ def get_address(rai):
     scyjava.start_jvm()
     class_name = scyjava.to_python(util.Helpers.classNameSimple(rai))
     class_name_full = scyjava.to_python(util.Helpers.className(rai))
-    if class_name in ('ArrayImg', 'UnsafeImg'):
+    if class_name in ("ArrayImg", "UnsafeImg"):
         img_class = scyjava.jimport(class_name_full)
         access = jpype.JObject(rai, img_class).update(None)
         access_type = scyjava.to_python(util.Helpers.className(access))
 
-        if 'basictypelongaccess.unsafe' in access_type:
+        if "basictypelongaccess.unsafe" in access_type:
             return jpype.JObject(access, access_type).getAddress()
         else:
             raise ValueError("Excpected unsafe access but got {}".format(access_type))
 
     else:
-        raise ValueError("Excpected ArrayImg or UnsafeImg but got {}".format(class_name))
+        raise ValueError(
+            "Excpected ArrayImg or UnsafeImg but got {}".format(class_name)
+        )
 
 
 class ImgLibReferenceGuard(np.ndarray):
@@ -95,11 +99,18 @@ def __new__(cls, rai):
         dtype = dtype_selector[imglib_type]
 
         # Create a ctypes pointer to the data
-        pointer = ctypes.cast(address, ctypes.POINTER(ctype_conversions_imglib[imglib_type]))
-        order = 'C'
-
-        # Create a new numpy array 
-        obj = np.ndarray.__new__(cls, buffer=np.ctypeslib.as_array(pointer, shape=shape), shape=shape, dtype=dtype)
+        pointer = ctypes.cast(
+            address, ctypes.POINTER(ctype_conversions_imglib[imglib_type])
+        )
+        order = "C"
+
+        # Create a new numpy array
+        obj = np.ndarray.__new__(
+            cls,
+            buffer=np.ctypeslib.as_array(pointer, shape=shape),
+            shape=shape,
+            dtype=dtype,
+        )
         obj.setflags(write=True)
         # Maintain a reference to the java object
         obj.rai = rai
@@ -112,6 +123,8 @@ def __array_finalize__(self, obj):
 
 
 HANDLED_FUNCTIONS = {}
+
+
 class NumpyView(np.ndarray):
     def __new__(cls, rai):
         access = rai.randomAccess()
@@ -125,7 +138,7 @@ def __new__(cls, rai):
         # Maintain a reference to the java object
         obj.rai = rai
         return obj
-    
+
     def __init__(self, rai):
         self.rai = rai
 
@@ -133,22 +146,24 @@ def __array_finalize__(self, obj):
         if obj is None:
             return
         self.rai = obj.rai
-    
+
     def __array_function__(self, func, types, args, kwargs):
         if func not in HANDLED_FUNCTIONS:
             return NotImplemented
-            # Note: this allows subclasses that don't override
+            # Note: this allows subclasses that don"t override
             # __array_function__ to handle DiagonalArray objects.
         if not all(issubclass(t, self.__class__) for t in types):
             return NotImplemented
         return HANDLED_FUNCTIONS[func](*args, **kwargs)
-    
+
     def __getitem__(self, key):
         if isinstance(key, slice):
-            print('Got a slice')
+            print("Got a slice")
         elif isinstance(key, int):
-            Views = scyjava.jimport('net.imglib2.view.Views')
-            return NumpyView(Views.hyperSlice(self.rai, self.rai.numDimensions() - 1, key))
+            Views = scyjava.jimport("net.imglib2.view.Views")
+            return NumpyView(
+                Views.hyperSlice(self.rai, self.rai.numDimensions() - 1, key)
+            )
         elif isinstance(key, tuple):
             ra = self.rai.randomAccess()
             for i in range(len(key)):
@@ -158,64 +173,76 @@ def __getitem__(self, key):
                 print(val)
             return ra.get().get()
         else:
-            raise ValueError(f'Cannot parse key {key}')
-
+            raise ValueError(f"Cannot parse key {key}")
 
     def __setitem__(self, key, value):
-        # print(f'key: {key}')
+        # print(f"key: {key}")
         # return 0
         if isinstance(key, slice):
-            print('Got a slice')
+            print("Got a slice")
         elif isinstance(key, int):
-            print('Got an int')
+            print("Got an int")
         elif isinstance(key, tuple):
             ra = self.rai.randomAccess()
             for i in range(len(key)):
-                ra.setPosition(key[i] % self.shape[i], len(key) -1 - i)
+                ra.setPosition(key[i] % self.shape[i], len(key) - 1 - i)
             ra.get().set(value)
         else:
-            raise ValueError(f'Cannot parse key {key}')
-    
-    def all(self): return np.all(self)
-    def any(self): return np.any(self)
+            raise ValueError(f"Cannot parse key {key}")
+
+    def all(self):
+        return np.all(self)
+
+    def any(self):
+        return np.any(self)
 
 
 def implements(np_function):
     "Register an __array_function__ implementation for DiagonalArray objects."
+
     def decorator(func):
         HANDLED_FUNCTIONS[np_function] = func
         return func
+
     return decorator
 
 
 @implements(np.all)
 def any(arr: NumpyView):
     for l in np.ndindex(*arr.shape):
-        if not arr[l]: return False
+        if not arr[l]:
+            return False
     return True
 
 
 @implements(np.any)
 def any(arr: NumpyView):
     for l in np.ndindex(*arr.shape):
-        if arr[l]: return True
+        if arr[l]:
+            return True
     return False
 
 
 if __name__ == "__main__":
-    ArrayImgs = scyjava.jimport('net.imglib2.img.array.ArrayImgs')
-    UnsafeUtil = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.UnsafeUtil')
-    Arrays = scyjava.jimport('java.util.Arrays')
-    OwningFloatUnsafe = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.owning.OwningFloatUnsafe')
-    Fraction = scyjava.jimport('net.imglib2.util.Fraction')
-    LongStream = scyjava.jimport('java.util.stream.LongStream')
+    ArrayImgs = scyjava.jimport("net.imglib2.img.array.ArrayImgs")
+    UnsafeUtil = scyjava.jimport(
+        "net.imglib2.img.basictypelongaccess.unsafe.UnsafeUtil"
+    )
+    Arrays = scyjava.jimport("java.util.Arrays")
+    OwningFloatUnsafe = scyjava.jimport(
+        "net.imglib2.img.basictypelongaccess.unsafe.owning.OwningFloatUnsafe"
+    )
+    Fraction = scyjava.jimport("net.imglib2.util.Fraction")
+    LongStream = scyjava.jimport("java.util.stream.LongStream")
 
     shape = (2, 3, 4)
     n_elements = int(np.prod(shape))
     data_store = OwningFloatUnsafe(n_elements)
     dim_array = LongStream.of(*shape).toArray()
     print(Arrays.toString(dim_array))
-    rai = util.Helpers.toArrayImg(jpype.JObject(util.Helpers.className(data_store), data_store), dim_array)
+    rai = util.Helpers.toArrayImg(
+        jpype.JObject(util.Helpers.className(data_store), data_store), dim_array
+    )
     # rai = ArrayImgs.floats( *shape )
     c = rai.cursor()
     count = 23
diff --git a/imglyb/ndarray_like_as_img.py b/src/imglyb/ndarray_like_as_img.py
similarity index 57%
rename from imglyb/ndarray_like_as_img.py
rename to src/imglyb/ndarray_like_as_img.py
index 3c85af5..309a16f 100644
--- a/imglyb/ndarray_like_as_img.py
+++ b/src/imglyb/ndarray_like_as_img.py
@@ -16,81 +16,109 @@
 
 _logger = logging.getLogger(__name__)
 
+
 def _java_setup():
     """
     Lazy initialization function for Java-dependent data structures.
     Do not call this directly; use scyjava.start_jvm() instead.
     """
     global PythonHelpers
-    PythonHelpers = scyjava.jimport('net.imglib2.python.Helpers')
+    PythonHelpers = scyjava.jimport("net.imglib2.python.Helpers")
 
     # non-owning
     global _ByteUnsafe
-    _ByteUnsafe = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.ByteUnsafe')
+    _ByteUnsafe = scyjava.jimport(
+        "net.imglib2.img.basictypelongaccess.unsafe.ByteUnsafe"
+    )
     global _CharUnsafe
-    _CharUnsafe = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.CharUnsafe')
+    _CharUnsafe = scyjava.jimport(
+        "net.imglib2.img.basictypelongaccess.unsafe.CharUnsafe"
+    )
     global _DoubleUnsafe
-    _DoubleUnsafe = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.DoubleUnsafe')
+    _DoubleUnsafe = scyjava.jimport(
+        "net.imglib2.img.basictypelongaccess.unsafe.DoubleUnsafe"
+    )
     global _FloatUnsafe
-    _FloatUnsafe = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.FloatUnsafe')
+    _FloatUnsafe = scyjava.jimport(
+        "net.imglib2.img.basictypelongaccess.unsafe.FloatUnsafe"
+    )
     global _IntUnsafe
-    _IntUnsafe = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.IntUnsafe')
+    _IntUnsafe = scyjava.jimport("net.imglib2.img.basictypelongaccess.unsafe.IntUnsafe")
     global _LongUnsafe
-    _LongUnsafe = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.LongUnsafe')
+    _LongUnsafe = scyjava.jimport(
+        "net.imglib2.img.basictypelongaccess.unsafe.LongUnsafe"
+    )
     global _ShortUnsafe
-    _ShortUnsafe = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.ShortUnsafe')
+    _ShortUnsafe = scyjava.jimport(
+        "net.imglib2.img.basictypelongaccess.unsafe.ShortUnsafe"
+    )
 
     global _unsafe_for_dtype
     _unsafe_for_dtype = {
-        np.dtype('complex64')  : _FloatUnsafe,
-        np.dtype('complex128') : _DoubleUnsafe,
-        np.dtype('float32')    : _FloatUnsafe,
-        np.dtype('float64')    : _DoubleUnsafe,
-        np.dtype('int8')       : _ByteUnsafe,
-        np.dtype('int16')      : _ShortUnsafe,
-        np.dtype('int32')      : _IntUnsafe,
-        np.dtype('int64')      : _LongUnsafe,
-        np.dtype('uint8')      : _ByteUnsafe,
-        np.dtype('uint16')     : _ShortUnsafe,
-        np.dtype('uint32')     : _IntUnsafe,
-        np.dtype('uint64')     : _LongUnsafe
+        np.dtype("complex64"): _FloatUnsafe,
+        np.dtype("complex128"): _DoubleUnsafe,
+        np.dtype("float32"): _FloatUnsafe,
+        np.dtype("float64"): _DoubleUnsafe,
+        np.dtype("int8"): _ByteUnsafe,
+        np.dtype("int16"): _ShortUnsafe,
+        np.dtype("int32"): _IntUnsafe,
+        np.dtype("int64"): _LongUnsafe,
+        np.dtype("uint8"): _ByteUnsafe,
+        np.dtype("uint16"): _ShortUnsafe,
+        np.dtype("uint32"): _IntUnsafe,
+        np.dtype("uint64"): _LongUnsafe,
     }
 
     # owning
     global _OwningByteUnsafe
-    _OwningByteUnsafe = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.owning.OwningByteUnsafe')
+    _OwningByteUnsafe = scyjava.jimport(
+        "net.imglib2.img.basictypelongaccess.unsafe.owning.OwningByteUnsafe"
+    )
     global _OwningCharUnsafe
-    _OwningCharUnsafe = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.owning.OwningCharUnsafe')
+    _OwningCharUnsafe = scyjava.jimport(
+        "net.imglib2.img.basictypelongaccess.unsafe.owning.OwningCharUnsafe"
+    )
     global _OwningDoubleUnsafe
-    _OwningDoubleUnsafe = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.owning.OwningDoubleUnsafe')
+    _OwningDoubleUnsafe = scyjava.jimport(
+        "net.imglib2.img.basictypelongaccess.unsafe.owning.OwningDoubleUnsafe"
+    )
     global _OwningFloatUnsafe
-    _OwningFloatUnsafe = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.owning.OwningFloatUnsafe')
+    _OwningFloatUnsafe = scyjava.jimport(
+        "net.imglib2.img.basictypelongaccess.unsafe.owning.OwningFloatUnsafe"
+    )
     global _OwningIntUnsafe
-    _OwningIntUnsafe = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.owning.OwningIntUnsafe')
+    _OwningIntUnsafe = scyjava.jimport(
+        "net.imglib2.img.basictypelongaccess.unsafe.owning.OwningIntUnsafe"
+    )
     global _OwningLongUnsafe
-    _OwningLongUnsafe = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.owning.OwningLongUnsafe')
+    _OwningLongUnsafe = scyjava.jimport(
+        "net.imglib2.img.basictypelongaccess.unsafe.owning.OwningLongUnsafe"
+    )
     global _OwningShortUnsafe
-    _OwningShortUnsafe = scyjava.jimport('net.imglib2.img.basictypelongaccess.unsafe.owning.OwningShortUnsafe')
+    _OwningShortUnsafe = scyjava.jimport(
+        "net.imglib2.img.basictypelongaccess.unsafe.owning.OwningShortUnsafe"
+    )
 
     global _unsafe_owning_for_dtype
     _unsafe_owning_for_dtype = {
-        np.dtype('complex64')  : lambda size: _OwningFloatUnsafe(2 * size),
-        np.dtype('complex128') : lambda size: _OwningDoubleUnsafe(2 * size),
-        np.dtype('float32')    : _OwningFloatUnsafe,
-        np.dtype('float64')    : _OwningDoubleUnsafe,
-        np.dtype('int8')       : _OwningByteUnsafe,
-        np.dtype('int16')      : _OwningShortUnsafe,
-        np.dtype('int32')      : _OwningIntUnsafe,
-        np.dtype('int64')      : _OwningLongUnsafe,
-        np.dtype('uint8')      : _OwningByteUnsafe,
-        np.dtype('uint16')     : _OwningShortUnsafe,
-        np.dtype('uint32')     : _OwningIntUnsafe,
-        np.dtype('uint64')     : _OwningLongUnsafe
+        np.dtype("complex64"): lambda size: _OwningFloatUnsafe(2 * size),
+        np.dtype("complex128"): lambda size: _OwningDoubleUnsafe(2 * size),
+        np.dtype("float32"): _OwningFloatUnsafe,
+        np.dtype("float64"): _OwningDoubleUnsafe,
+        np.dtype("int8"): _OwningByteUnsafe,
+        np.dtype("int16"): _OwningShortUnsafe,
+        np.dtype("int32"): _OwningIntUnsafe,
+        np.dtype("int64"): _OwningLongUnsafe,
+        np.dtype("uint8"): _OwningByteUnsafe,
+        np.dtype("uint16"): _OwningShortUnsafe,
+        np.dtype("uint32"): _OwningIntUnsafe,
+        np.dtype("uint64"): _OwningLongUnsafe,
     }
 
     global MakeAccessFunction
-    @JImplements('java.util.function.LongFunction')
-    class MakeAccessFunction():
+
+    @JImplements("java.util.function.LongFunction")
+    class MakeAccessFunction:
         """
         Implements a java `LongFunction` that can be passed into `PythonHelpers.imgFromFunc` and
         `PythonHelpers.imgWithCellLoaderFromFunc`.
@@ -104,6 +132,7 @@ def apply(self, index):
             access = self.func(index)
             return access
 
+
 scyjava.when_jvm_starts(_java_setup)
 
 
@@ -117,8 +146,8 @@ def identity(x):
 def _chunk_index_to_slices(shape, chunk_shape, cell_index):
 
     grid_dimensions = tuple(
-        int(math.ceil(s/sh))
-        for s, sh in zip(shape, chunk_shape))[::-1]
+        int(math.ceil(s / sh)) for s, sh in zip(shape, chunk_shape)
+    )[::-1]
 
     chunk_min = []
     ndims = len(grid_dimensions)
@@ -127,13 +156,11 @@ def _chunk_index_to_slices(shape, chunk_shape, cell_index):
     for d in range(ndims):
         c = i % grid_dimensions[d]
         chunk_min.append(c)
-        i = (i - c)//grid_dimensions[d]
+        i = (i - c) // grid_dimensions[d]
 
     chunk_min = chunk_min[::-1]
 
-    slices = tuple(
-        slice(c*cs, (c + 1)*cs)
-        for c, cs in zip(chunk_min, chunk_shape))
+    slices = tuple(slice(c * cs, (c + 1) * cs) for c, cs in zip(chunk_min, chunk_shape))
 
     return slices
 
@@ -145,7 +172,9 @@ def _get_chunk(array, chunk_shape, chunk_index, chunk_as_array):
     return np.ascontiguousarray(array)
 
 
-def _get_chunk_access_array(array, chunk_shape, index, chunk_as_array, use_volatile_access=True):
+def _get_chunk_access_array(
+    array, chunk_shape, index, chunk_as_array, use_volatile_access=True
+):
     try:
         chunk = _get_chunk(array, chunk_shape, index, chunk_as_array)
         dtype = types.for_np_dtype(chunk.dtype, volatile=False)
@@ -153,26 +182,29 @@ def _get_chunk_access_array(array, chunk_shape, index, chunk_as_array, use_volat
         # TODO check ratio for integral value first?
         ratio = int(dtype.getEntitiesPerPixel().getRatio())
         return accesses.Accesses.asArrayAccess(
-            util._get_address(chunk),
-            chunk.size * ratio,
-            use_volatile_access,
-            ptype)
+            util._get_address(chunk), chunk.size * ratio, use_volatile_access, ptype
+        )
 
     except JException as e:
         _logger.error(scyjava.jstacktrace(e))
         raise e
 
 
-def _get_chunk_access_unsafe(array, chunk_shape, index, chunk_as_array, reference_store):
+def _get_chunk_access_unsafe(
+    array, chunk_shape, index, chunk_as_array, reference_store
+):
 
     try:
-        chunk = np.ascontiguousarray(_get_chunk(array, chunk_shape, index, chunk_as_array))
+        chunk = np.ascontiguousarray(
+            _get_chunk(array, chunk_shape, index, chunk_as_array)
+        )
         address = util._get_address(chunk)
         ref_id = reference_store.get_next_id()
 
         def remove_reference():
-            _logger.debug('Removing ref id %d', ref_id)
+            _logger.debug("Removing ref id %d", ref_id)
             reference_store.remove_reference(ref_id)
+
         owner = util.RunnableFromFunc(remove_reference)
         reference_store.add_reference(ref_id, (chunk, owner))
         access = _access_factory_for(chunk.dtype, owning=False)(address, owner)
@@ -183,7 +215,15 @@ def remove_reference():
         raise e
 
 
-def as_cell_img(array, chunk_shape, cache, *, access_type='native', chunk_as_array=identity, **kwargs):
+def as_cell_img(
+    array,
+    chunk_shape,
+    cache,
+    *,
+    access_type="native",
+    chunk_as_array=identity,
+    **kwargs,
+):
     """
     Wrap an arbitrary ndarray-like object as an ImgLib2 cached cell img.
 
@@ -205,24 +245,37 @@ def as_cell_img(array, chunk_shape, cache, *, access_type='native', chunk_as_arr
     scyjava.start_jvm()
 
     access_type_function_mapping = {
-        'array':  as_cell_img_with_array_accesses,
-        'native': as_cell_img_with_native_accesses
+        "array": as_cell_img_with_array_accesses,
+        "native": as_cell_img_with_native_accesses,
     }
 
     if access_type not in access_type_function_mapping:
-        raise Exception(f'Invalid access type: `{access_type}\'. Choose one of {access_type_function_mapping.keys()}')
+        raise Exception(
+            f"Invalid access type: `{access_type}'. Choose one of {access_type_function_mapping.keys()}"
+        )
 
     cache = caches.BoundedSoftRefLoaderCache(cache) if isinstance(cache, int) else cache
 
-    return access_type_function_mapping[access_type](array, chunk_shape, chunk_as_array, cache, **kwargs)
+    return access_type_function_mapping[access_type](
+        array, chunk_shape, chunk_as_array, cache, **kwargs
+    )
 
 
 # TODO is it bad style to use **kwargs to ignore unexpected kwargs?
-def as_cell_img_with_array_accesses(array, chunk_shape, chunk_as_array, cache, *, use_volatile_access=True, **kwargs):
+def as_cell_img_with_array_accesses(
+    array, chunk_shape, chunk_as_array, cache, *, use_volatile_access=True, **kwargs
+):
     scyjava.start_jvm()
 
     access_generator = MakeAccessFunction(
-        lambda index: _get_chunk_access_array(array, chunk_shape, index, chunk_as_array, use_volatile_access=use_volatile_access))
+        lambda index: _get_chunk_access_array(
+            array,
+            chunk_shape,
+            index,
+            chunk_as_array,
+            use_volatile_access=use_volatile_access,
+        )
+    )
     reference_store = ReferenceStore()
     reference_store.add_reference_with_new_id(access_generator)
 
@@ -238,19 +291,26 @@ def as_cell_img_with_array_accesses(array, chunk_shape, chunk_as_array, cache, *
         # TODO do not load first block here, just create a length-one access
         accesses.as_array_access(
             _get_chunk(array, chunk_shape, 0, chunk_as_array=chunk_as_array),
-            volatile=use_volatile_access),
-        cache)
+            volatile=use_volatile_access,
+        ),
+        cache,
+    )
 
     return img, reference_store
 
 
 # TODO is it bad style to use **kwargs to ignore unexpected kwargs?
-def as_cell_img_with_native_accesses(array, chunk_shape, chunk_as_array, cache, **kwargs):
+def as_cell_img_with_native_accesses(
+    array, chunk_shape, chunk_as_array, cache, **kwargs
+):
     scyjava.start_jvm()
 
     reference_store = ReferenceStore()
     access_generator = MakeAccessFunction(
-        lambda index: _get_chunk_access_unsafe(array, chunk_shape, index, chunk_as_array, reference_store))
+        lambda index: _get_chunk_access_unsafe(
+            array, chunk_shape, index, chunk_as_array, reference_store
+        )
+    )
     reference_store.add_reference_with_new_id(access_generator)
 
     shape = array.shape[::-1]
@@ -263,7 +323,8 @@ def as_cell_img_with_native_accesses(array, chunk_shape, chunk_as_array, cache,
             access_generator,
             types.for_np_dtype(array.dtype, volatile=False),
             _access_factory_for(array.dtype, owning=False)(1, None),
-            cache)
+            cache,
+        )
 
     except JException as e:
         _logger.error(jstacktrace(e))
@@ -274,5 +335,3 @@ def as_cell_img_with_native_accesses(array, chunk_shape, chunk_as_array, cache,
 
 def _access_factory_for(dtype, owning):
     return _unsafe_owning_for_dtype[dtype] if owning else _unsafe_for_dtype[dtype]
-
-
diff --git a/imglyb/reference_store.py b/src/imglyb/reference_store.py
similarity index 91%
rename from imglyb/reference_store.py
rename to src/imglyb/reference_store.py
index 8b6c505..f1fbc75 100644
--- a/imglyb/reference_store.py
+++ b/src/imglyb/reference_store.py
@@ -1,13 +1,14 @@
 import threading
 
-'''
+"""
+
+"""
 
-'''
-class ReferenceStore(object):
 
+class ReferenceStore(object):
     def __init__(self):
-        self.lock    = threading.RLock()
-        self.store   = dict()
+        self.lock = threading.RLock()
+        self.store = dict()
         self.next_id = 0
 
     def add_reference(self, ref_id, reference):
diff --git a/src/imglyb/types.py b/src/imglyb/types.py
new file mode 100644
index 0000000..f49c545
--- /dev/null
+++ b/src/imglyb/types.py
@@ -0,0 +1,103 @@
+import numpy as np
+import scyjava
+
+
+def _java_setup():
+    """
+    Lazy initialization function for Java-dependent data structures.
+    Do not call this directly; use scyjava.start_jvm() instead.
+    """
+
+    global NativeType
+    NativeType = scyjava.jimport("net.imglib2.type.NativeType")
+
+    global FloatType
+    FloatType = scyjava.jimport("net.imglib2.type.numeric.real.FloatType")
+    global DoubleType
+    DoubleType = scyjava.jimport("net.imglib2.type.numeric.real.DoubleType")
+    global ByteType
+    ByteType = scyjava.jimport("net.imglib2.type.numeric.integer.ByteType")
+    global UnsignedByteType
+    UnsignedByteType = scyjava.jimport(
+        "net.imglib2.type.numeric.integer.UnsignedByteType"
+    )
+    global ShortType
+    ShortType = scyjava.jimport("net.imglib2.type.numeric.integer.ShortType")
+    global UnsignedShortType
+    UnsignedShortType = scyjava.jimport(
+        "net.imglib2.type.numeric.integer.UnsignedShortType"
+    )
+    global IntType
+    IntType = scyjava.jimport("net.imglib2.type.numeric.integer.IntType")
+    global UnsignedIntType
+    UnsignedIntType = scyjava.jimport(
+        "net.imglib2.type.numeric.integer.UnsignedIntType"
+    )
+    global LongType
+    LongType = scyjava.jimport("net.imglib2.type.numeric.integer.LongType")
+    global UnsignedLongType
+    UnsignedLongType = scyjava.jimport(
+        "net.imglib2.type.numeric.integer.UnsignedLongType"
+    )
+
+    global VolatileFloatType
+    VolatileFloatType = scyjava.jimport("net.imglib2.type.volatiles.VolatileFloatType")
+    global VolatileDoubleType
+    VolatileDoubleType = scyjava.jimport(
+        "net.imglib2.type.volatiles.VolatileDoubleType"
+    )
+    global VolatileByteType
+    VolatileByteType = scyjava.jimport("net.imglib2.type.volatiles.VolatileByteType")
+    global VolatileUnsignedByteType
+    VolatileUnsignedByteType = scyjava.jimport(
+        "net.imglib2.type.volatiles.VolatileUnsignedByteType"
+    )
+    global VolatileShortType
+    VolatileShortType = scyjava.jimport("net.imglib2.type.volatiles.VolatileShortType")
+    global VolatileUnsignedShortType
+    VolatileUnsignedShortType = scyjava.jimport(
+        "net.imglib2.type.volatiles.VolatileUnsignedShortType"
+    )
+    global VolatileIntType
+    VolatileIntType = scyjava.jimport("net.imglib2.type.volatiles.VolatileIntType")
+    global VolatileUnsignedIntType
+    VolatileUnsignedIntType = scyjava.jimport(
+        "net.imglib2.type.volatiles.VolatileUnsignedIntType"
+    )
+    global VolatileLongType
+    VolatileLongType = scyjava.jimport("net.imglib2.type.volatiles.VolatileLongType")
+    global VolatileUnsignedLongType
+    VolatileUnsignedLongType = scyjava.jimport(
+        "net.imglib2.type.volatiles.VolatileUnsignedLongType"
+    )
+
+
+scyjava.when_jvm_starts(_java_setup)
+
+
+def for_np_dtype(dtype, volatile=False):
+    scyjava.start_jvm()
+    if dtype == np.uint8:
+        return VolatileUnsignedByteType() if volatile else UnsignedByteType()
+    if dtype == np.int8:
+        return VolatileByteType() if volatile else ByteType()
+
+    if dtype == np.uint16:
+        return VolatileUnsignedShortType() if volatile else UnsignedShortType()
+    if dtype == np.int16:
+        return VolatileShortType() if volatile else ShortType()
+
+    if dtype == np.uint32:
+        return VolatileUnsignedIntType() if volatile else UnsignedIntType()
+    if dtype == np.int32:
+        return VolatileIntType() if volatile else IntType()
+
+    if dtype == np.uint64:
+        return VolatileUnsignedLongType() if volatile else UnsignedLongType()
+    if dtype == np.int64:
+        return VolatileLongType() if volatile else LongType()
+
+    if dtype == np.float32:
+        return VolatileFloatType() if volatile else FloatType()
+    if dtype == np.float64:
+        return VolatileDoubleType() if volatile else DoubleType()
diff --git a/src/imglyb/util.py b/src/imglyb/util.py
new file mode 100644
index 0000000..d4c8a45
--- /dev/null
+++ b/src/imglyb/util.py
@@ -0,0 +1,177 @@
+import logging
+
+import numpy as np
+import scyjava
+
+from jpype import JImplements, JOverride, JLong, JArray
+
+_logger = logging.getLogger(__name__)
+
+__all__ = ("to_imglib", "to_imglib_argb")
+
+
+def _java_setup():
+    """
+    Lazy initialization function for Java-dependent data structures.
+    Do not call this directly; use scyjava.start_jvm() instead.
+    """
+
+    # java
+    global Random
+    Random = scyjava.jimport("java.util.Random")
+
+    # imglib
+    try:
+        global Helpers
+        Helpers = scyjava.jimport("net.imglib2.python.Helpers")
+        global NumpyToImgLibConversions
+        NumpyToImgLibConversions = scyjava.jimport(
+            "net.imglib2.python.NumpyToImgLibConversions"
+        )
+        global NumpyToImgLibConversionsWithStride
+        NumpyToImgLibConversionsWithStride = scyjava.jimport(
+            "net.imglib2.python.NumpyToImgLibConversionsWithStride"
+        )
+        global Views
+        Views = scyjava.jimport("net.imglib2.view.Views")
+    except TypeError as e:
+        _logger.error(
+            "Failed to import ImgLib2 helper classes. Please ensure imglib2-imglyb is present on the classpath."
+        )
+        raise e
+
+    # Guard
+    global ReferenceGuardingRandomAccessibleInterval
+    ReferenceGuardingRandomAccessibleInterval = scyjava.jimport(
+        "net.imglib2.python.ReferenceGuardingRandomAccessibleInterval"
+    )
+
+    global numpy_dtype_to_conversion_method
+    numpy_dtype_to_conversion_method = {
+        np.dtype("complex64"): NumpyToImgLibConversions.toComplexFloat,
+        np.dtype("complex128"): NumpyToImgLibConversions.toComplexDouble,
+        np.dtype("float32"): NumpyToImgLibConversions.toFloat,
+        np.dtype("float64"): NumpyToImgLibConversions.toDouble,
+        np.dtype("int8"): NumpyToImgLibConversions.toByte,
+        np.dtype("int16"): NumpyToImgLibConversions.toShort,
+        np.dtype("int32"): NumpyToImgLibConversions.toInt,
+        np.dtype("int64"): NumpyToImgLibConversions.toLong,
+        np.dtype("uint8"): NumpyToImgLibConversions.toUnsignedByte,
+        np.dtype("uint16"): NumpyToImgLibConversions.toUnsignedShort,
+        np.dtype("uint32"): NumpyToImgLibConversions.toUnsignedInt,
+        np.dtype("uint64"): NumpyToImgLibConversions.toUnsignedLong,
+    }
+
+    global numpy_dtype_to_conversion_with_stride_method
+    numpy_dtype_to_conversion_with_stride_method = {
+        np.dtype("complex64"): NumpyToImgLibConversionsWithStride.toComplexFloat,
+        np.dtype("complex128"): NumpyToImgLibConversionsWithStride.toComplexDouble,
+        np.dtype("float32"): NumpyToImgLibConversionsWithStride.toFloat,
+        np.dtype("float64"): NumpyToImgLibConversionsWithStride.toDouble,
+        np.dtype("int8"): NumpyToImgLibConversionsWithStride.toByte,
+        np.dtype("int16"): NumpyToImgLibConversionsWithStride.toShort,
+        np.dtype("int32"): NumpyToImgLibConversionsWithStride.toInt,
+        np.dtype("int64"): NumpyToImgLibConversionsWithStride.toLong,
+        np.dtype("uint8"): NumpyToImgLibConversionsWithStride.toUnsignedByte,
+        np.dtype("uint16"): NumpyToImgLibConversionsWithStride.toUnsignedShort,
+        np.dtype("uint32"): NumpyToImgLibConversionsWithStride.toUnsignedInt,
+        np.dtype("uint64"): NumpyToImgLibConversionsWithStride.toUnsignedLong,
+    }
+
+    global ReferenceGuard
+
+    @JImplements(
+        "net.imglib2.python.ReferenceGuardingRandomAccessibleInterval$ReferenceHolder"
+    )
+    class ReferenceGuard:
+        def __init__(self, *args, **kwargs):
+            self.args = args
+
+    global GenericMouseMotionListener
+
+    @JImplements("java.awt.event.MouseMotionListener")
+    class GenericMouseMotionListener:
+        def __init__(self, mouse_dragged=lambda e: None, mouse_moved=lambda e: None):
+            self.mouse_dragged = mouse_dragged
+            self.mouse_moved = mouse_moved
+
+        @JOverride
+        def mouseDragged(self, e):
+            self.mouse_dragged(e)
+
+        @JOverride
+        def mouseMoved(self, e):
+            self.mouse_moved(e)
+
+    global RunnableFromFunc
+
+    @JImplements("java.lang.Runnable")
+    class RunnableFromFunc:
+        def __init__(self, func):
+            self.func = func
+
+        @JOverride
+        def run(self):
+            _logger.debug("Running function %s", self.func)
+            self.func()
+
+
+scyjava.when_jvm_starts(_java_setup)
+
+
+def _get_address(source):
+    return source.ctypes.data
+
+
+# how to use type hints for python < 3.5?
+def to_imglib(source):
+    scyjava.start_jvm()
+    return ReferenceGuardingRandomAccessibleInterval(
+        _to_imglib(source), ReferenceGuard(source)
+    )
+
+
+# how to use type hints for python < 3.5?
+def _to_imglib(source):
+    address = _get_address(source)
+    long_address = JLong(address)
+    long_arr_source = JArray(JLong)(source.shape[::-1])
+
+    if not source.dtype in numpy_dtype_to_conversion_method:
+        raise NotImplementedError(
+            "Cannot convert dtype to ImgLib2 type yet: {}".format(source.dtype)
+        )
+    elif source.flags["CARRAY"]:
+        return numpy_dtype_to_conversion_method[source.dtype](
+            long_address, *long_arr_source
+        )
+    else:
+        stride = np.array(source.strides[::-1]) / source.itemsize
+        long_arr_stride = JArray(JLong)(stride)
+        return numpy_dtype_to_conversion_with_stride_method[source.dtype](
+            long_address, long_arr_stride, long_arr_source
+        )
+
+
+def to_imglib_argb(source):
+    scyjava.start_jvm()
+    return ReferenceGuardingRandomAccessibleInterval(
+        _to_imglib_argb(source), ReferenceGuard(source)
+    )
+
+
+def _to_imglib_argb(source):
+    address = _get_address(source)
+    long_address = JLong(address)
+    long_arr_source = JArray(JLong)(source.shape[::-1])
+
+    if not (source.dtype == np.dtype("int32") or source.dtype == np.dtype("uint32")):
+        raise NotImplementedError("source.dtype must be int32 or uint32")
+    if source.flags["CARRAY"]:
+        return NumpyToImgLibConversions.toARGB(long_address, *long_arr_source)
+    else:
+        stride = np.array(source.strides[::-1]) / source.itemsize
+        long_arr_stride = JArray(JLong)(stride)
+        return NumpyToImgLibConversionsWithStride.toARGB(
+            long_address, long_arr_stride, long_arr_source
+        )


Test patch:
diff --git a/.github/workflows/python-test-conda.yml b/.github/workflows/python-test-conda.yml
deleted file mode 100644
index 5db1008..0000000
--- a/.github/workflows/python-test-conda.yml
+++ /dev/null
@@ -1,38 +0,0 @@
-name: build
-
-on:
-  push:
-    branches:
-      - master
-    tags:
-      - "*-[0-9]+.*"
-  pull_request:
-    branches:
-      - master
-
-jobs:
-  build-linux:
-    runs-on: ubuntu-latest
-
-    steps:
-    - uses: actions/checkout@v2
-    - name: Add conda to system path
-      run: |
-        # $CONDA is an environment variable pointing to the root of the miniconda directory
-        echo $CONDA/bin >> $GITHUB_PATH
-    - name: Install mamba
-      run: |
-        conda install -c conda-forge mamba
-    - name: Install dependencies
-      run: |
-        mamba env update --file environment-test.yml --name base
-    - name: Install current project in dev mode
-      run: |
-        pip install -e .
-    - name: Install pytest
-      run: |
-        mamba install -c conda-forge pytest
-    - name: Test with pytest
-      run: |
-        pytest -p no:faulthandler
-
diff --git a/conftest.py b/conftest.py
index e62d6d3..c8c438e 100644
--- a/conftest.py
+++ b/conftest.py
@@ -1,14 +1,16 @@
+from re import S
 import scyjava
 import pytest
+import jgo, logging, sys
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def sj_fixture(request):
     """
     Start the JVM through scyjava once for the whole test environment
     :param request: Pytest variable passed in to fixtures
     """
-    scyjava.config.add_option('-Djava.awt.headless=true')
+    scyjava.config.add_option("-Djava.awt.headless=true")
     scyjava.start_jvm()
 
     yield scyjava
diff --git a/environment-test.yml b/environment-test.yml
deleted file mode 100644
index 6b2b9f2..0000000
--- a/environment-test.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-# You can use this environment to run the tests, when imglyb will be
-# installed from source
-name: imglyb
-channels:
-  - conda-forge
-  - defaults
-dependencies:
-  - numpy
-  - scyjava
diff --git a/test/test_imglyb.py b/test/test_imglyb.py
index 6842962..962aa8d 100644
--- a/test/test_imglyb.py
+++ b/test/test_imglyb.py
@@ -7,33 +7,31 @@
 
 from imglyb.imglib_ndarray import ImgLibReferenceGuard, NumpyView
 
+
 class TestImglyb(object):
     def test_arraylike(self, sj_fixture):
         Views = imglyb.util.Views
 
-        shape      = (3, 5)
-        data       = np.arange(np.prod(shape)).reshape(shape)
+        shape = (3, 5)
+        data = np.arange(np.prod(shape)).reshape(shape)
         block_size = (2, 2)
-        img, _     = imglyb.as_cell_img(data, block_size, access_type='native', cache=1)
+        img, _ = imglyb.as_cell_img(data, block_size, access_type="native", cache=1)
 
         cursor = Views.flatIterable(img).cursor()
         expected = 0
         while cursor.hasNext():
             assert expected == cursor.next().get()
             expected += 1
-    
 
     @pytest.fixture
     def unsafe_img(self):
-        UnsafeImgs = scyjava.jimport('net.imglib2.img.unsafe.UnsafeImgs')
+        UnsafeImgs = scyjava.jimport("net.imglib2.img.unsafe.UnsafeImgs")
         return UnsafeImgs.bytes(2, 3, 4)
 
-   
     def test_to_numpy_returns_ndarray(self, unsafe_img):
         thing = imglyb.to_numpy(unsafe_img)
         assert isinstance(thing, np.ndarray)
 
-
     def test_to_numpy_modification(self, unsafe_img):
         thing = imglyb.to_numpy(unsafe_img)
 
@@ -42,21 +40,22 @@ def test_to_numpy_modification(self, unsafe_img):
         thing.fill(fill_value)
 
         cursor = unsafe_img.cursor()
-        while(cursor.hasNext()):
+        while cursor.hasNext():
             assert cursor.next().get() == fill_value
 
+
 class TestRAIAsNumpyArray:
     @pytest.fixture
     def img(self):
-        ArrayImgs = scyjava.jimport('net.imglib2.img.array.ArrayImgs')
+        ArrayImgs = scyjava.jimport("net.imglib2.img.array.ArrayImgs")
         img = ArrayImgs.unsignedBytes(2, 3, 4)
         tmp_val = 1
         cursor = img.cursor()
-        while(cursor.hasNext()):
+        while cursor.hasNext():
             cursor.next().set(tmp_val)
             tmp_val = tmp_val + 1
         return img
-    
+
     @pytest.fixture
     def raiAsNumpyArray(self, img):
         # populate each index with a unique value
@@ -68,14 +67,13 @@ def test_get_tuple(self, img, raiAsNumpyArray):
         arr[:] = [1, 1, 1]
         j_val = ra.setPositionAndGet(arr).get()
         assert j_val == raiAsNumpyArray[1, 1, 1]
-    
+
     def test_get_int(self, img, raiAsNumpyArray):
         for slice_val in range(len(raiAsNumpyArray)):
             slice = raiAsNumpyArray[slice_val]
             for i in range(slice.shape[0]):
                 for j in range(slice.shape[1]):
                     assert slice[i, j] == raiAsNumpyArray[slice_val, i, j]
-    
 
     def test_change_rai(self, img, raiAsNumpyArray):
         """Tests that changes in the img affect the numpy array"""
@@ -98,7 +96,7 @@ def test_change_ndarray(self, img, raiAsNumpyArray):
 
     def test_size(self, img, raiAsNumpyArray: np.ndarray):
         """Tests any behavior."""
-        Intervals = scyjava.jimport('net.imglib2.util.Intervals')
+        Intervals = scyjava.jimport("net.imglib2.util.Intervals")
         assert Intervals.numElements(img) == raiAsNumpyArray.size
 
     @pytest.fixture
@@ -120,7 +118,6 @@ def test_all(self, img, raiAsNumpyArray: np.ndarray):
             assert raiAsNumpyArray.all() == False
             assert np.all(raiAsNumpyArray) == False
 
-
     def test_any(self, img, raiAsNumpyArray: np.ndarray):
         """Tests any behavior."""
         # simple_img starts out with a one -> Should be true
@@ -135,6 +132,3 @@ def test_any(self, img, raiAsNumpyArray: np.ndarray):
         # now should be false
         assert raiAsNumpyArray.any() == False
         assert np.any(raiAsNumpyArray) == False
-
-
-        
\ No newline at end of file
diff --git a/test/test_version.py b/test/test_version.py
new file mode 100644
index 0000000..d9bf2b9
--- /dev/null
+++ b/test/test_version.py
@@ -0,0 +1,86 @@
+import os
+import sys
+
+import pytest
+
+setuptools_file = os.path.join(os.getcwd(), "src", "imglyb", "version.py")
+
+
+def _imglyb_version():
+    """
+    Get ImgLyb's version.
+    """
+    import imglyb
+
+    # It's important that we clear the cache here,
+    # so that we can test different behaviors.
+    imglyb.___version__.cache_clear()
+    # Get the version
+    return imglyb.__version__
+
+
+def test_version_file():
+    """Ensures that, ideally, the version from setuptools_scm is used"""
+    # Get the version from setuptools_scm
+    from setuptools_scm import get_version
+
+    setuptools_version = get_version(write_to="src/imglyb/version.py")
+    # Ensure that the version was written to file
+    assert os.path.isfile(setuptools_file)
+    # Ensure that imglyb.__version__ matches this.
+    assert _imglyb_version() == setuptools_version
+    # Cleanup - remove file
+    os.remove(setuptools_file)
+    assert not os.path.isfile(setuptools_file)
+
+
+@pytest.mark.skipif(sys.version_info < (3, 8), reason="Requires Python >= 3.8")
+def test_version_importlib():
+    """
+    Ensures that, with imglyb.version.version unavailable,
+    importlib.metadata is used next WITH python 3.8+
+    """
+    # Remove imglyb.version
+    sys.modules["imglyb.version"] = None
+    # Ensure imglyb.__version__ matches importlib.metadata.version()
+    from importlib.metadata import version
+
+    assert _imglyb_version() == version("imglyb")
+
+
+@pytest.mark.skipif(
+    sys.version_info >= (3, 8), reason="importlib used instead for Python 3.8+"
+)
+def test_version_pkg_resources():
+    """
+    Ensures that, with imglyb.version.version AND
+    importlib.metadata unavailable,
+    pkg_resources is used next.
+    """
+    # Remove imglyb.version
+    sys.modules["imglyb.version"] = None
+    # Remove importlib.metadata
+    sys.modules["importlib.metadata"] = None
+    # Ensure imglyb.__version__ matches
+    # pkg_resources.get_distribution().version
+    from pkg_resources import get_distribution
+
+    assert _imglyb_version() == get_distribution("imglyb").version
+
+
+def test_version_unvailable():
+    """
+    Ensures that no version is returned if none of these
+    strategies works.
+    """
+    # Remove imglyb.version
+    sys.modules["imglyb.version"] = None
+    # Remove importlib.metadata
+    sys.modules["importlib.metadata"] = None
+    # Remove pkg_resources
+    sys.modules["pkg_resources"] = None
+    # Ensure imglyb.__version__ is an error message.
+    assert (
+        _imglyb_version()
+        == "Cannot determine version! Ensure pkg_resources is installed!"
+    )