diff --git a/.cruft.json b/.cruft.json new file mode 100644 index 0000000..994935f --- /dev/null +++ b/.cruft.json @@ -0,0 +1,21 @@ +{ + "template": "https://github.com/tlambert03/pyrepo-cookiecutter", + "commit": "a9545077bbb7a5c6d62addbe8a3484f844e7d5b2", + "context": { + "cookiecutter": { + "full_name": "Talley Lambert", + "email": "talley.lambert@gmail.com", + "github_username": "tlambert03", + "project_name": "nd2", + "project_slug": "nd2", + "project_short_description": "Yet another nd2 (Nikon NIS Elements) file reader", + "pypi_username": "talley", + "version_control": "setuptools-scm", + "_copy_without_render": [ + ".github/workflows/*" + ], + "_template": "https://github.com/tlambert03/pyrepo-cookiecutter" + } + }, + "directory": null +} diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..7ca09ef --- /dev/null +++ b/.flake8 @@ -0,0 +1,7 @@ +[flake8] +profile = black +exclude = docs,.eggs,examples +max-line-length = 88 +docstring-convention = numpy +docstring_style = numpy +ignore = D100, D213, D401, D413, D107, W503 diff --git a/.github/TEST_FAIL_TEMPLATE.md b/.github/TEST_FAIL_TEMPLATE.md new file mode 100644 index 0000000..21d3c5f --- /dev/null +++ b/.github/TEST_FAIL_TEMPLATE.md @@ -0,0 +1,12 @@ +--- +title: "{{ env.TITLE }}" +labels: [bug] +--- +The {{ workflow }} workflow failed on {{ date | date("YYYY-MM-DD HH:mm") }} UTC + +The most recent failing test was on {{ env.PLATFORM }} py{{ env.PYTHON }} +with commit: {{ sha }} + +Full run: https://github.com/{{ payload.repository.full_name }}/actions/runs/{{ env.RUN_ID }} + +(This post will be updated if another test fails, as long as this issue remains open.) diff --git a/.github/workflows/test.yml b/.github/workflows/ci.yml similarity index 75% rename from .github/workflows/test.yml rename to .github/workflows/ci.yml index 633ae81..dc18fdd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Test +name: CI on: push: @@ -12,6 +12,14 @@ on: workflow_dispatch: jobs: + check-manifest: + name: Check Manifest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + - run: pip install check-manifest && check-manifest + test: name: ${{ matrix.platform }} (${{ matrix.python-version }}) runs-on: ${{ matrix.platform }} @@ -22,14 +30,14 @@ jobs: platform: [macos-latest, windows-latest, "ubuntu-latest"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - - uses: actions/cache@v2 + - uses: actions/cache@v3 id: cache with: path: tests/data @@ -43,7 +51,7 @@ jobs: - name: Build # -e seems necessary for coverage to work - run: pip install -e .[testing] + run: pip install -e .[test] env: CYTHON_TRACE: "1" diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml new file mode 100644 index 0000000..42e1470 --- /dev/null +++ b/.github/workflows/cron.yml @@ -0,0 +1,62 @@ +name: --pre Test +# An "early warning" cron job that will install dependencies +# with `pip install --pre` periodically to test for breakage +# (and open an issue if a test fails) + +on: + schedule: + - cron: '0 */12 * * *' # every 12 hours + workflow_dispatch: + +jobs: + + test: + name: ${{ matrix.platform }} (${{ matrix.python-version }}) + runs-on: ${{ matrix.platform }} + strategy: + fail-fast: false + matrix: + python-version: ['3.8', '3.9', '3.10'] + platform: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - uses: actions/cache@v3 + id: cache + with: + path: tests/data + key: ${{ hashFiles('scripts/download_samples.py') }} + + - name: Download Samples + if: steps.cache.outputs.cache-hit != 'true' + run: | + pip install requests + python scripts/download_samples.py + + - name: Build + run: pip install --pre -e .[test] + env: + CYTHON_TRACE: "1" + + - name: Test + run: pytest -v --cov=nd2 --cov-report=xml --cov-report=term + + # If something goes wrong, we can open an issue in the repo + - name: Report Failures + if: ${{ failure() }} + uses: JasonEtco/create-an-issue@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PLATFORM: ${{ matrix.platform }} + PYTHON: ${{ matrix.python }} + RUN_ID: ${{ github.run_id }} + TITLE: '[test-bot] pip install --pre is failing' + with: + filename: .github/TEST_FAIL_TEMPLATE.md + update_existing: true diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 805f2c5..bf9241b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -18,9 +18,9 @@ jobs: os: [ubuntu-20.04, windows-2019, macos-10.15] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v3 name: Install Python with: python-version: "3.9" @@ -30,7 +30,7 @@ jobs: python -m pip install --upgrade pip pip install build - - uses: actions/cache@v2 + - uses: actions/cache@v3 id: cache with: path: tests/data @@ -43,9 +43,9 @@ jobs: python scripts/download_samples.py - name: Build wheels - uses: pypa/cibuildwheel@v2.2.0a1 + uses: pypa/cibuildwheel@2.6.0 - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: path: ./wheelhouse/*.whl @@ -56,7 +56,7 @@ jobs: check-manifest python -m build --sdist - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 if: matrix.os == 'ubuntu-20.04' with: path: dist/*.tar.gz @@ -66,12 +66,12 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v') steps: - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v3 with: name: artifact path: dist - - uses: pypa/gh-action-pypi-publish@v1.4.2 + - uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.TWINE_API_KEY }} diff --git a/.github_changelog_generator b/.github_changelog_generator new file mode 100644 index 0000000..03e0caa --- /dev/null +++ b/.github_changelog_generator @@ -0,0 +1,5 @@ +user=tlambert03 +project=nd2 +issues=false +exclude-labels=duplicate,question,invalid,wontfix,hide +add-sections={"tests":{"prefix":"**Tests & CI:**","labels":["tests"]}} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3553e74..14f1ea9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,41 +1,62 @@ +ci: + autoupdate_schedule: monthly + autofix_commit_msg: "style: [pre-commit.ci] auto fixes [...]" + autoupdate_commit_msg: "ci: [pre-commit.ci] autoupdate" + +default_install_hook_types: [pre-commit, commit-msg] + exclude: src/sdk + repos: + + - repo: https://github.com/compilerla/conventional-pre-commit + rev: v1.3.0 + hooks: + - id: conventional-pre-commit + stages: [commit-msg] + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.2.0 hooks: - id: check-docstring-first - id: end-of-file-fixer - id: trailing-whitespace - - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.20.1 - hooks: - - id: setup-cfg-fmt - - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 - hooks: - - id: flake8 - additional_dependencies: [flake8-typing-imports==1.7.0] + - repo: https://github.com/myint/autoflake rev: v1.4 hooks: - id: autoflake args: ["--in-place", "--remove-all-unused-imports"] + - repo: https://github.com/PyCQA/isort rev: 5.10.1 hooks: - id: isort - - repo: https://github.com/psf/black - rev: 22.3.0 - hooks: - - id: black + - repo: https://github.com/asottile/pyupgrade rev: v2.32.1 hooks: - id: pyupgrade args: [--py37-plus, --keep-runtime-typing] + + - repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black + + - repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 + hooks: + - id: flake8 + args: [--min-python-version=3.7.0] + additional_dependencies: + - flake8-typing-imports + - flake8-bugbear + - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.950 + rev: v0.960 hooks: - id: mypy - additional_dependencies: [numpy] + files: "^src/" exclude: scripts + additional_dependencies: [numpy] diff --git a/pyproject.toml b/pyproject.toml index fd9675e..920aa2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,9 @@ [build-system] requires = [ - "setuptools", + "setuptools>=45", "wheel", "cython", - "numpy", + "setuptools-scm>=6.2", "numpy==1.14.5; python_version=='3.7'", "numpy==1.17.3; python_version=='3.8'", "numpy==1.19.3; python_version=='3.9'", @@ -11,12 +11,168 @@ requires = [ ] build-backend = "setuptools.build_meta" + +[project] +name = "nd2" +description = "Yet another nd2 (Nikon NIS Elements) file reader" +readme = "README.md" +requires-python = ">=3.7,<3.11" +license = {file = "LICENSE"} +authors = [ + {email = "talley.lambert@gmail.com"}, + {name = "Talley Lambert"}, +] +classifiers = [ + "Development Status :: 3 - Alpha", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", +] +dynamic = ["version"] +dependencies = [ + "lxml", + "resource-backed-dask-array", + "typing-extensions", + "numpy>=1.14.5;python_version=='3.7'", + "numpy>=1.17.3;python_version=='3.8'", + "numpy>=1.19.3;python_version=='3.9'", + "numpy>=1.21.3;python_version=='3.10'", +] + +[project.optional-dependencies] +legacy = [ + "imagecodecs", + "wurlizter", +] +test = [ + "aicsimageio", + "Cython", + "dask[array]", + "imagecodecs", + "psutil", + "pytest-cov", + "pytest>=6.0", + "wurlitzer", + "xarray", +] +dev = [ + "aicsimageio", + "black", + "cruft", + "Cython", + "dask[array]", + "flake8-bugbear", + "flake8-typing-imports", + "flake8", + "imagecodecs", + "ipython", + "isort", + "mypy", + "pre-commit", + "psutil", + "pydocstyle", + "pytest-cov", + "pytest", + "rich", + "wurlitzer", + "xarray", +] + +[project.urls] +homepage = "https://github.com/tlambert03/nd2" +repository = "https://github.com/tlambert03/nd2" +# documentation = "readthedocs.org" +# changelog = "github.com/me/spam/blob/master/CHANGELOG.md" + +# https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html +[tool.setuptools] +zip-safe = false +include-package-data = true + +[tool.setuptools.packages.find] +where = ["src"] # list of folders that contain the packages (["."] by default) +include = ["nd2*"] # package names should match these glob patterns (["*"] by default) + +# https://github.com/pypa/setuptools_scm/#pyprojecttoml-usage +[tool.setuptools_scm] +write_to = "src/nd2/_version.py" + +# https://pycqa.github.io/isort/docs/configuration/options.html +[tool.isort] +profile = "black" +src_paths = ["src/nd2", "tests"] + +# http://www.pydocstyle.org/en/stable/usage.html +[tool.pydocstyle] +match_dir = "src/nd2" +convention = "numpy" +add_select = "D402,D415,D417" +ignore = "D100,D213,D401,D413,D107" + +# https://docs.pytest.org/en/6.2.x/customize.html +[tool.pytest.ini_options] +minversion = "6.0" +addopts = '--color=yes' +testpaths = ["tests"] +filterwarnings = [ + "error", + "ignore:The distutils package is deprecated::", + "ignore:The distutils.sysconfig module is deprecated::", +] + +# https://mypy.readthedocs.io/en/stable/config_file.html +[tool.mypy] +files = "src/nd2" +warn_unused_configs = true +warn_unused_ignores = true +check_untyped_defs = true +implicit_reexport = false +show_column_numbers = true +show_error_codes = true +ignore_missing_imports = true +pretty = true + +[[tool.mypy.overrides]] +module = 'nd2.structures' +ignore_errors = true + +# https://coverage.readthedocs.io/en/6.4/config.html +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "if TYPE_CHECKING:", + "@overload", + "except ImportError", +] + +# https://github.com/cruft/cruft +[tool.cruft] +skip = ["tests"] + +# https://github.com/mgedmin/check-manifest#configuration +[tool.check-manifest] +ignore = [ + ".cruft.json", + ".flake8", + ".github_changelog_generator", + ".pre-commit-config.yaml", + "tests/**/*", + "tox.ini", + "src/nd2/_version.py" +] +ignore-bad-ideas = [ + "*.so" +] + [tool.cibuildwheel] # Skip 32-bit builds & PyPy wheels on all platforms skip = ["*-win32", "*-manylinux_i686", "pp*", "*musllinux*"] test-requires = "pytest" test-command = 'pytest "{project}/tests" -v' -test-extras = ["testing"] +test-extras = ["test"] manylinux-x86_64-image = "manylinux_2_24" [tool.cibuildwheel.macos] @@ -31,3 +187,11 @@ before-all = [ "apt-get update && apt-get install -y libtiff5-dev", "cp {project}/src/sdk/Linux/x86_64/lib/* /lib", ] + +# https://python-semantic-release.readthedocs.io/en/latest/configuration.html +[tool.semantic_release] +version_source = "tag_only" +branch = "main" +changelog_sections="feature,fix,breaking,documentation,performance,chore,:boom:,:sparkles:,:children_crossing:,:lipstick:,:iphone:,:egg:,:chart_with_upwards_trend:,:ambulance:,:lock:,:bug:,:zap:,:goal_net:,:alien:,:wheelchair:,:speech_balloon:,:mag:,:apple:,:penguin:,:checkered_flag:,:robot:,:green_apple:,Other" +# commit_parser=semantic_release.history.angular_parser +build_command = "pip install build && python -m build" diff --git a/scripts/bf_describe.py b/scripts/bf_describe.py index 1ccab86..63b0348 100644 --- a/scripts/bf_describe.py +++ b/scripts/bf_describe.py @@ -2,6 +2,7 @@ from pathlib import Path from aicsimageio.readers.bioformats_reader import BioFile, BioformatsReader + from nd2._util import AXIS DATA = Path(__file__).parent.parent / "tests" / "data" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index d0fe21c..0000000 --- a/setup.cfg +++ /dev/null @@ -1,121 +0,0 @@ -[metadata] -name = nd2 -description = Yet another nd2 (Nikon NIS Elements) file reader. -long_description = file: README.md -long_description_content_type = text/markdown -url = https://github.com/tlambert03/nd2 -author = Talley Lambert -author_email = talley.lambert@gmail.com -license = BSD-3-Clause -license_file = LICENSE -classifiers = - Development Status :: 2 - Pre-Alpha - License :: OSI Approved :: BSD License - Natural Language :: English - Programming Language :: Python :: 3 - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 -project_urls = - Source Code =https://github.com/tlambert03/nd2 - -[options] -packages = nd2 -install_requires = - lxml - resource-backed-dask-array - typing-extensions - numpy>=1.14.5;python_version=='3.7' - numpy>=1.17.3;python_version=='3.8' - numpy>=1.19.3;python_version=='3.9' - numpy>=1.21.3;python_version=='3.10' -python_requires = >=3.7,<3.11 -include_package_data = True -package_dir = - =src -setup_requires = - setuptools-scm -zip_safe = False - -[options.extras_require] -dev = - Cython - black - dask[array] - flake8 - flake8-docstrings - imagecodecs - ipython - isort - mypy - pre-commit - psutil - pydocstyle - pytest - pytest - pytest-cov - wurlitzer - xarray -legacy = - imagecodecs - wurlitzer -testing = - aicsimageio - cython - dask[array] - psutil - pytest - pytest-cov - wurlitzer - xarray - imagecodecs;sys_platform != "darwin" or python_version < "3.10" - -[options.package_data] -mypackage = py.typed - -[bdist_wheel] -universal = 1 - -[flake8] -exclude = docs,_version.py,.eggs,examples -max-line-length = 88 -docstring-convention = numpy -ignore = D100, D213, D401, D413, D107, W503 - -[isort] -profile = black -src_paths = nd2 - -[pydocstyle] -match_dir = nd2 -convention = numpy -add_select = D402,D415,D417 -ignore = D100, D213, D401, D413, D107 - -[tool:pytest] -addopts = --color=yes -filterwarnings = - error - ignore:The distutils package is deprecated:: - ignore:The distutils.sysconfig module is deprecated:: - -[check-manifest] -ignore = - src/nd2/_version.py -ignore-bad-ideas = - *.so - -[mypy] -files = src/nd2 -warn_unused_configs = True -warn_unused_ignores = True -check_untyped_defs = True -implicit_reexport = False -show_column_numbers = True -show_error_codes = True -ignore_missing_imports = True - -[mypy-nd2.structures] -ignore_errors = True diff --git a/src/nd2/_chunkmap.py b/src/nd2/_chunkmap.py index 90edeb2..bd3c9e3 100644 --- a/src/nd2/_chunkmap.py +++ b/src/nd2/_chunkmap.py @@ -235,13 +235,16 @@ def iter_chunks(handle) -> Iterator[Tuple[str, int, int]]: handle.seek(pos) +_default_chunk_start = CHUNK_MAGIC.to_bytes(4, "little") + + def rescue_nd2( handle: Union[BinaryIO, str], frame_shape: Tuple[int, ...] = (), dtype: DTypeLike = "uint16", max_iters: Optional[int] = None, verbose=True, - chunk_start=CHUNK_MAGIC.to_bytes(4, "little"), + chunk_start: bytes = _default_chunk_start, ): """Iterator that yields all discovered frames in a file handle @@ -268,6 +271,9 @@ def rescue_nd2( max_iters : Optional[int], optional A maximum number of frames to yield, by default will yield until the end of the file is reached + verbose : bool + whether to print info + chunk_start Yields ------ diff --git a/src/nd2/nd2file.py b/src/nd2/nd2file.py index fd0fdb1..17e479d 100644 --- a/src/nd2/nd2file.py +++ b/src/nd2/nd2file.py @@ -3,7 +3,6 @@ import mmap import threading from enum import Enum -from functools import lru_cache from itertools import product from pathlib import Path from typing import ( @@ -61,12 +60,12 @@ def __init__( ---------- path : Union[Path, str] Filename of an nd2 file. - validate_frames : bool, optional + validate_frames : bool Whether to verify (and attempt to fix) frames whose positions have been shifted relative to the predicted offset (i.e. in a corrupted file). This comes at a slight performance penalty at file open, but may "rescue" some corrupt files. by default False. - search_window : int, optional + search_window : int When validate_frames is true, this is the search window (in KB) that will be used to try to find the actual chunk position. by default 100 KB """ @@ -149,7 +148,6 @@ def metadata(self) -> Union[Metadata, dict]: """Various metadata (will be dict if legacy format).""" return self._rdr.metadata() - @lru_cache(maxsize=1024) def frame_metadata( self, seq_index: Union[int, tuple] ) -> Union[FrameMetadata, dict]: @@ -157,7 +155,18 @@ def frame_metadata( This includes the global metadata from the metadata function. (will be dict if legacy format). + + Parameters + ---------- + seq_index : Union[int, tuple] + frame index + + Returns + ------- + Union[FrameMetadata, dict] + dict if legacy format, else FrameMetadata """ + idx = cast( int, self._seq_index_from_coords(seq_index) @@ -229,7 +238,18 @@ def dtype(self) -> np.dtype: return np.dtype(f"{d}{attrs.bitsPerComponentInMemory // 8}") def voxel_size(self, channel: int = 0) -> VoxelSize: - """XYZ voxel size.""" + """XYZ voxel size. + + Parameters + ---------- + channel : int + Channel for which to retrieve voxel info, by default 0 + + Returns + ------- + VoxelSize + Named tuple with attrs `x`, `y`, and `z`. + """ return VoxelSize(*self._rdr.voxel_size()) def asarray(self, position: Optional[int] = None) -> np.ndarray: @@ -246,8 +266,10 @@ def asarray(self, position: Optional[int] = None) -> np.ndarray: Raises ------ + ValueError + if `position` is a string and is not a valid position name IndexError - if position is provided and is out of range + if `position` is provided and is out of range """ final_shape = list(self.shape) if position is None: @@ -471,8 +493,19 @@ def _get_frame(self, index: int) -> np.ndarray: frame.shape = self._raw_frame_shape return frame.transpose((2, 0, 1, 3)).squeeze() - def _expand_coords(self, squeeze=True) -> dict: - """Return a dict that can be used as the coords argument to xr.DataArray""" + def _expand_coords(self, squeeze: bool = True) -> dict: + """Return a dict that can be used as the coords argument to xr.DataArray + + Parameters + ---------- + squeeze : bool + whether to squeeze axes with length < 2, by default True + + Returns + ------- + dict + dict of axis name -> coordinates + """ dx, dy, dz = self.voxel_size() coords: Dict[str, Sized] = { @@ -483,7 +516,7 @@ def _expand_coords(self, squeeze=True) -> dict: } for c in self.experiment: - if squeeze and getattr(c, "count") <= 1: + if squeeze and c.count <= 1: continue if c.type == "ZStackLoop": coords[AXIS.Z] = np.arange(c.count) * c.parameters.stepUm diff --git a/tests/conftest.py b/tests/conftest.py index 26d4abf..8ede257 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ import psutil import pytest + from nd2._util import is_new_format DATA = Path(__file__).parent / "data" diff --git a/tests/test_dask_dispatch.py b/tests/test_dask_dispatch.py index edc5e7b..0694dff 100644 --- a/tests/test_dask_dispatch.py +++ b/tests/test_dask_dispatch.py @@ -1,9 +1,10 @@ import dask.array as da import numpy as np import pytest -from nd2 import ND2File from resource_backed_dask_array import ResourceBackedDaskArray +from nd2 import ND2File + @pytest.mark.parametrize("leave_open", [True, False]) @pytest.mark.parametrize("wrapper", [True, False]) diff --git a/tests/test_reader.py b/tests/test_reader.py index 34b923e..bd0a69c 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -8,9 +8,10 @@ import numpy as np import pytest import xarray as xr +from resource_backed_dask_array import ResourceBackedDaskArray + from nd2 import ND2File, imread, structures from nd2._util import AXIS -from resource_backed_dask_array import ResourceBackedDaskArray DATA = Path(__file__).parent / "data" diff --git a/tests/test_rescue.py b/tests/test_rescue.py index 94ffb46..2884deb 100644 --- a/tests/test_rescue.py +++ b/tests/test_rescue.py @@ -1,6 +1,7 @@ -import nd2 import numpy as np +import nd2 + def test_rescue(single_nd2): # TODO: we could potentially put more of this logic into convenience functions diff --git a/tests/test_sdk.py b/tests/test_sdk.py index f898743..024bbe7 100644 --- a/tests/test_sdk.py +++ b/tests/test_sdk.py @@ -2,6 +2,7 @@ import numpy as np import pytest + from nd2._sdk import latest diff --git a/tests/test_segfaults.py b/tests/test_segfaults.py index 81fbbb6..3f75505 100644 --- a/tests/test_segfaults.py +++ b/tests/test_segfaults.py @@ -1,8 +1,9 @@ from pathlib import Path -import nd2 import numpy as np +import nd2 + DATA = Path(__file__).parent / "data"