diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7a1b3c..d3c7646 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,26 +30,17 @@ jobs: fail-fast: false matrix: platform: [macos-latest, windows-latest, "ubuntu-latest"] - python-version: ["3.9", "3.10", "3.11"] - include: - - python-version: "3.7" - platform: "ubuntu-latest" - - python-version: "3.8" - platform: "ubuntu-latest" - - python-version: "3.8" - platform: "windows-latest" - - python-version: "3.12-dev" - platform: "ubuntu-latest" + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: cache with: path: tests/data @@ -69,17 +60,19 @@ jobs: run: pytest -v --cov=nd2 --cov-report=xml --cov-report=term - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} benchmarks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.11" - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: cache with: path: tests/data @@ -95,7 +88,7 @@ jobs: run: python -m pip install .[test] - name: Run benchmarks - uses: CodSpeedHQ/action@v1 + uses: CodSpeedHQ/action@v2 with: run: pytest --codspeed -v --color=yes @@ -108,7 +101,7 @@ jobs: with: fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.x" diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml index 3d0c162..ab37f5b 100644 --- a/.github/workflows/cron.yml +++ b/.github/workflows/cron.yml @@ -19,8 +19,6 @@ jobs: python-version: ['3.9', '3.10', '3.11'] platform: [ubuntu-latest, macos-latest, windows-latest] include: - - python-version: "3.7" - platform: "ubuntu-latest" - python-version: "3.8" platform: "ubuntu-latest" - python-version: "3.8" @@ -30,11 +28,11 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - uses: actions/cache@v3 + - uses: actions/cache@v4 id: cache with: path: tests/data diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3c70ce9..f417618 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.x diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3422668..bdb43dc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,23 +20,23 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.4 + rev: v0.2.0 hooks: - id: ruff args: [--fix] - repo: https://github.com/psf/black - rev: 23.10.1 + rev: 24.1.1 hooks: - id: black - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.15 + rev: v0.16 hooks: - id: validate-pyproject - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.6.1 + rev: v1.8.0 hooks: - id: mypy files: "^src/" diff --git a/README.md b/README.md index d20ec2d..07d9c45 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ `.nd2` (Nikon NIS Elements) file reader. -This reader provides a pure python implementation the official Nikon ND2 SDK. +This reader provides a pure python implementation of the Nikon ND2 SDK. > It _used_ to wrap the official SDK with Cython, but has since been completely > rewritten to be pure python (for performance, ease of distribution, and @@ -73,7 +73,7 @@ import numpy as np my_array = nd2.imread('some_file.nd2') # read to numpy array my_array = nd2.imread('some_file.nd2', dask=True) # read to dask array my_array = nd2.imread('some_file.nd2', xarray=True) # read to xarray -my_array = nd2.imread('some_file.nd2', xarray=True, dask=True) # read file to dask-xarray +my_array = nd2.imread('some_file.nd2', xarray=True, dask=True) # read to dask-xarray # or open a file with nd2.ND2File f = nd2.ND2File('some_file.nd2') diff --git a/pyproject.toml b/pyproject.toml index ec0f0a9..6cf7345 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,14 +7,13 @@ build-backend = "hatchling.build" name = "nd2" description = "Yet another nd2 (Nikon NIS Elements) file reader" readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.8" license = { text = "BSD 3-Clause License" } authors = [{ email = "talley.lambert@gmail.com", name = "Talley Lambert" }] classifiers = [ "Development Status :: 4 - Beta", "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", @@ -30,7 +29,7 @@ test = [ "aicsimageio; python_version < '3.12'", "dask[array]", "imagecodecs", - "lxml", + "lxml; python_version >= '3.9'", "numpy >=1.26; python_version >= '3.12'", "numpy; python_version < '3.12'", "ome-types", @@ -69,9 +68,6 @@ documentation = "https://tlambert03.github.io/nd2/" [tool.hatch.version] source = "vcs" -[tool.hatch.build.hooks.vcs] -version-file = "src/nd2/_version.py" - [tool.hatch.build.targets.wheel] only-include = ["src"] sources = ["src"] @@ -79,7 +75,7 @@ sources = ["src"] # https://beta.ruff.rs/docs/rules/ [tool.ruff] line-length = 88 -target-version = "py37" +target-version = "py38" src = ["src/nd2", "tests"] select = [ "E", # style errors diff --git a/scripts/gather.py b/scripts/gather.py index b7eaccc..2907fcb 100644 --- a/scripts/gather.py +++ b/scripts/gather.py @@ -1,4 +1,5 @@ """gather metadata from all files in test/data with all nd readers.""" + import contextlib import json from pathlib import Path diff --git a/scripts/nd2_describe.py b/scripts/nd2_describe.py index 169f943..af14fda 100644 --- a/scripts/nd2_describe.py +++ b/scripts/nd2_describe.py @@ -4,6 +4,7 @@ python scripts/nd2_describe.py > tests/samples_metadata.json """ + import struct from dataclasses import asdict from pathlib import Path diff --git a/src/nd2/__init__.py b/src/nd2/__init__.py index 440df98..b1c1d64 100644 --- a/src/nd2/__init__.py +++ b/src/nd2/__init__.py @@ -1,9 +1,12 @@ """nd2: A Python library for reading and writing ND2 files.""" +from importlib.metadata import PackageNotFoundError, version + try: - from ._version import version as __version__ -except ImportError: + __version__ = version(__name__) +except PackageNotFoundError: # pragma: no cover __version__ = "unknown" + __author__ = "Talley Lambert" __email__ = "talley.lambert@gmail.com" __all__ = [ diff --git a/src/nd2/_binary.py b/src/nd2/_binary.py index 50cdf9e..d21880f 100644 --- a/src/nd2/_binary.py +++ b/src/nd2/_binary.py @@ -1,4 +1,5 @@ """Utilities for binary layers in ND2 files.""" + from __future__ import annotations import io @@ -131,12 +132,10 @@ def __init__(self, data: list[BinaryLayer]) -> None: self._data = data @overload - def __getitem__(self, key: int) -> BinaryLayer: - ... + def __getitem__(self, key: int) -> BinaryLayer: ... @overload - def __getitem__(self, key: slice) -> list[BinaryLayer]: - ... + def __getitem__(self, key: slice) -> list[BinaryLayer]: ... def __getitem__(self, key: int | slice) -> BinaryLayer | list[BinaryLayer]: return self._data[key] diff --git a/src/nd2/_parse/_chunk_decode.py b/src/nd2/_parse/_chunk_decode.py index 00201f3..22330bb 100644 --- a/src/nd2/_parse/_chunk_decode.py +++ b/src/nd2/_parse/_chunk_decode.py @@ -1,4 +1,5 @@ """FIXME: this has a lot of code duplication with _chunkmap.py.""" + from __future__ import annotations import mmap diff --git a/src/nd2/_parse/_legacy_xml.py b/src/nd2/_parse/_legacy_xml.py index 23c36be..06371be 100644 --- a/src/nd2/_parse/_legacy_xml.py +++ b/src/nd2/_parse/_legacy_xml.py @@ -5,6 +5,7 @@ all of this logic is duplicated in _clx_xml.py. _legacy.py just needs some slight updates to deal with different parsing results. """ + from __future__ import annotations import re diff --git a/src/nd2/_parse/_parse.py b/src/nd2/_parse/_parse.py index db48df2..ec5fc01 100644 --- a/src/nd2/_parse/_parse.py +++ b/src/nd2/_parse/_parse.py @@ -584,7 +584,7 @@ def load_metadata(raw_meta: RawMetaDict, global_meta: GlobalMetadata) -> strct.M if matrix and (matrix.get("Columns") == 2 and matrix.get("Rows") == 2): # matrix["Data"] is a list of int64, we need to recast to float data = bytearray(matrix["Data"]) - matrix_data: tuple[float, float, float, float] = tuple( # type: ignore + matrix_data: tuple[float, float, float, float] = tuple( i[0] for i in strctd.iter_unpack(data) ) volume["cameraTransformationMatrix"] = matrix_data @@ -637,7 +637,8 @@ def load_metadata(raw_meta: RawMetaDict, global_meta: GlobalMetadata) -> strct.M channel=channel_meta, loops=loops, microscope=strct.Microscope( - **microscope, modalityFlags=flags # type: ignore + **microscope, + modalityFlags=flags, # type: ignore ), volume=strct.Volume( **volume, diff --git a/src/nd2/_sdk_types.py b/src/nd2/_sdk_types.py index 0b775c7..0ea101b 100644 --- a/src/nd2/_sdk_types.py +++ b/src/nd2/_sdk_types.py @@ -1,11 +1,14 @@ """Various raw dict structures likely to be found in an ND2 file.""" + from __future__ import annotations from enum import IntEnum, auto -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing_extensions import Literal, NotRequired, TypeAlias, TypedDict + from typing import Literal, TypedDict, Union + + from typing_extensions import NotRequired, TypeAlias class RawAttributesDict(TypedDict, total=False): uiWidth: int diff --git a/src/nd2/_util.py b/src/nd2/_util.py index 5c26564..11428cf 100644 --- a/src/nd2/_util.py +++ b/src/nd2/_util.py @@ -2,18 +2,14 @@ import math import re -import warnings from datetime import datetime, timezone from itertools import product from typing import TYPE_CHECKING, BinaryIO, NamedTuple, cast if TYPE_CHECKING: from os import PathLike - from typing import Any, Callable, ClassVar, Mapping, Sequence, Union + from typing import Any, Callable, ClassVar, Final, Mapping, Sequence, Union - from typing_extensions import Final - - from nd2.readers import ND2Reader from nd2.structures import ExpLoop StrOrPath = Union[str, PathLike] @@ -77,19 +73,6 @@ def is_legacy(path: StrOrPath) -> bool: return fh.read(4) == OLD_HEADER_MAGIC -def get_reader( - path: str, validate_frames: bool = False, search_window: int = 100 -) -> ND2Reader: # pragma: no cover - warnings.warn( - "Deprecated, use nd2.readers.ND2Reader.create if you want to " - "directly instantiate a reader subclass.", - stacklevel=2, - ) - from nd2.readers import ND2Reader - - return ND2Reader.create(path, search_window * 1000 if validate_frames else None) - - def is_new_format(path: str) -> bool: # TODO: this is just for dealing with missing test data with open(path, "rb") as fh: @@ -159,9 +142,6 @@ def parse_time(time_str: str) -> datetime: raise ValueError(f"Could not parse {time_str}") # pragma: no cover -# utils for converting records to dicts, in recorded_data method - - def convert_records_to_dict_of_lists( records: ListOfDicts, null_val: Any = float("nan") ) -> DictOfLists: diff --git a/src/nd2/index.py b/src/nd2/index.py index fc80fbc..3e0de0b 100644 --- a/src/nd2/index.py +++ b/src/nd2/index.py @@ -7,9 +7,7 @@ from concurrent.futures import ThreadPoolExecutor from datetime import datetime from pathlib import Path -from typing import Any, Iterable, Iterator, Sequence, cast, no_type_check - -from typing_extensions import TypedDict +from typing import Any, Iterable, Iterator, Sequence, TypedDict, cast, no_type_check import nd2 diff --git a/src/nd2/nd2file.py b/src/nd2/nd2file.py index 6de81c0..67fbc01 100644 --- a/src/nd2/nd2file.py +++ b/src/nd2/nd2file.py @@ -20,13 +20,12 @@ if TYPE_CHECKING: from pathlib import Path - from typing import Any, Sequence, Sized, SupportsInt + from typing import Any, Literal, Sequence, Sized, SupportsInt import dask.array import dask.array.core import xarray as xr from ome_types import OME - from typing_extensions import Literal from ._binary import BinaryLayers from ._util import ( @@ -85,13 +84,6 @@ class ND2File: 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 - read_using_sdk : Optional[bool] - :warning: **DEPRECATED**. No longer does anything. - - If `True`, use the SDK to read the file. If `False`, inspects the chunkmap - and reads from a `numpy.memmap`. If `None` (the default), uses the SDK if - the file is compressed, otherwise uses the memmap. Note: using - `read_using_sdk=False` on a compressed file will result in a ValueError. """ def __init__( @@ -100,15 +92,7 @@ def __init__( *, validate_frames: bool = False, search_window: int = 100, - read_using_sdk: bool | None = None, ) -> None: - if read_using_sdk is not None: - warnings.warn( - "The `read_using_sdk` argument is deprecated and will be removed in " - "a future version.", - FutureWarning, - stacklevel=2, - ) self._error_radius: int | None = ( search_window * 1000 if validate_frames else None ) @@ -343,16 +327,17 @@ def experiment(self) -> list[ExpLoop]: @overload def events( self, *, orient: Literal["records"] = ..., null_value: Any = ... - ) -> ListOfDicts: - ... + ) -> ListOfDicts: ... @overload - def events(self, *, orient: Literal["list"], null_value: Any = ...) -> DictOfLists: - ... + def events( + self, *, orient: Literal["list"], null_value: Any = ... + ) -> DictOfLists: ... @overload - def events(self, *, orient: Literal["dict"], null_value: Any = ...) -> DictOfDicts: - ... + def events( + self, *, orient: Literal["dict"], null_value: Any = ... + ) -> DictOfDicts: ... def events( self, @@ -405,7 +390,6 @@ def unstructured_metadata( strip_prefix: bool = True, include: set[str] | None = None, exclude: set[str] | None = None, - unnest: bool | None = None, ) -> dict[str, Any]: """Exposes, and attempts to decode, each metadata chunk in the file. @@ -430,8 +414,6 @@ def unstructured_metadata( all metadata sections found in the file are included. exclude : set[str] | None, optional If provided, exclude the specified keys from the output. by default `None` - unnest : bool, optional - :warning: **DEPRECATED**. No longer does anything. Returns ------- @@ -440,12 +422,6 @@ def unstructured_metadata( metadata chunk (things like 'CustomData|RoiMetadata_v1' or 'ImageMetadataLV'), and values that are associated metadata chunk. """ - if unnest is not None: - warnings.warn( - "The unnest parameter is deprecated, and no longer has any effect.", - FutureWarning, - stacklevel=2, - ) return self._rdr.unstructured_metadata(strip_prefix, include, exclude) @cached_property @@ -717,9 +693,11 @@ def frame_metadata(self, seq_index: int | tuple) -> FrameMetadata | dict: """ idx = cast( int, - self._seq_index_from_coords(seq_index) - if isinstance(seq_index, tuple) - else seq_index, + ( + self._seq_index_from_coords(seq_index) + if isinstance(seq_index, tuple) + else seq_index + ), ) return self._rdr.frame_metadata(idx) @@ -1154,27 +1132,6 @@ def __repr__(self) -> str: extra = "" return f"" - @property - def recorded_data(self) -> DictOfLists: - """Return tabular data recorded for each frame of the experiment. - - !!! warning "Deprecated" - - This method is deprecated and will be removed in a future version. - Please use the [`events`][nd2.ND2File.events] method instead. To get the - same dict-of-lists output that `recorded_data` returns, use - `ndfile.events(orient='list')` - """ - warnings.warn( - "recorded_data is deprecated and will be removed in a future version." - "Please use the `events` method instead. To get the same dict-of-lists " - "output, use `events(orient='list')`", - FutureWarning, - stacklevel=2, - ) - - return self.events(orient="list") - @cached_property def binary_data(self) -> BinaryLayers | None: """Return binary layers embedded in the file. @@ -1232,9 +1189,7 @@ def imread( dask: Literal[False] = ..., xarray: Literal[False] = ..., validate_frames: bool = ..., - read_using_sdk: bool | None = None, -) -> np.ndarray: - ... +) -> np.ndarray: ... @overload @@ -1244,9 +1199,7 @@ def imread( dask: bool = ..., xarray: Literal[True], validate_frames: bool = ..., - read_using_sdk: bool | None = None, -) -> xr.DataArray: - ... +) -> xr.DataArray: ... @overload @@ -1256,9 +1209,7 @@ def imread( dask: Literal[True], xarray: Literal[False] = ..., validate_frames: bool = ..., - read_using_sdk: bool | None = None, -) -> dask.array.core.Array: - ... +) -> dask.array.core.Array: ... def imread( @@ -1267,7 +1218,6 @@ def imread( dask: bool = False, xarray: bool = False, validate_frames: bool = False, - read_using_sdk: bool | None = None, ) -> np.ndarray | xr.DataArray | dask.array.core.Array: """Open `file`, return requested array type, and close `file`. @@ -1290,23 +1240,13 @@ def imread( 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. - read_using_sdk : Optional[bool] - :warning: **DEPRECATED**. No longer used. - - If `True`, use the SDK to read the file. If `False`, inspects the chunkmap and - reads from a `numpy.memmap`. If `None` (the default), uses the SDK if the file - is compressed, otherwise uses the memmap. - Note: using `read_using_sdk=False` on a compressed file will result in a - ValueError. Returns ------- Union[np.ndarray, dask.array.Array, xarray.DataArray] Array subclass, depending on arguments used. """ - with ND2File( - file, validate_frames=validate_frames, read_using_sdk=read_using_sdk - ) as nd2: + with ND2File(file, validate_frames=validate_frames) as nd2: if xarray: return nd2.to_xarray(delayed=dask) elif dask: diff --git a/src/nd2/readers/__init__.py b/src/nd2/readers/__init__.py index f563882..4260263 100644 --- a/src/nd2/readers/__init__.py +++ b/src/nd2/readers/__init__.py @@ -1,4 +1,5 @@ """Reader subclasses for legacy and modern ND2 files.""" + from ._legacy.legacy_reader import LegacyReader from ._modern.modern_reader import ModernReader from .protocol import ND2Reader diff --git a/src/nd2/readers/_legacy/legacy_reader.py b/src/nd2/readers/_legacy/legacy_reader.py index 81e718f..9a0ed17 100644 --- a/src/nd2/readers/_legacy/legacy_reader.py +++ b/src/nd2/readers/_legacy/legacy_reader.py @@ -22,9 +22,7 @@ if TYPE_CHECKING: from collections import defaultdict - from typing import Any, BinaryIO, Mapping - - from typing_extensions import TypedDict + from typing import Any, BinaryIO, Mapping, TypedDict from nd2._util import FileOrBinaryIO @@ -432,7 +430,7 @@ def header(self) -> dict: def events(self, orient: str, null_value: Any) -> list | Mapping: warnings.warn( - "`recorded_data` is not implemented for legacy ND2 files", + "`events` is not implemented for legacy ND2 files", UserWarning, stacklevel=2, ) diff --git a/src/nd2/readers/_modern/modern_reader.py b/src/nd2/readers/_modern/modern_reader.py index 136165d..73d4721 100644 --- a/src/nd2/readers/_modern/modern_reader.py +++ b/src/nd2/readers/_modern/modern_reader.py @@ -31,8 +31,9 @@ if TYPE_CHECKING: import datetime from os import PathLike + from typing import Literal - from typing_extensions import Literal, TypeAlias + from typing_extensions import TypeAlias from nd2._binary import BinaryLayers from nd2._parse._chunk_decode import ChunkMap @@ -382,28 +383,21 @@ def _strides(self) -> tuple[int, ...] | None: if not (widthP and widthB): self._strides_ = None else: - bypc = self._bytes_per_pixel() - compCount = a.componentCount - array_stride = widthB - (bypc * widthP * compCount) - if array_stride == 0: + n_components = a.componentCount + bypc = a.bitsPerComponentInMemory // 8 + if widthB == (widthP * bypc * n_components): self._strides_ = None else: - self._strides_ = ( - array_stride + widthP * bypc * compCount, - compCount * bypc, - compCount // (a.channelCount or 1) * bypc, - bypc, - ) + # the extra bypc is because we shape this as + # (width, height, channels, RGBcompents) + # see _actual_frame_shape() below + self._strides_ = (widthB, n_components * bypc, bypc, bypc) return self._strides_ def _actual_frame_shape(self) -> tuple[int, ...]: attr = self.attributes() - return ( - attr.heightPx, - attr.widthPx or 1, - attr.channelCount or 1, - attr.componentCount // (attr.channelCount or 1), - ) + nC = attr.channelCount or 1 + return (attr.heightPx, attr.widthPx or 1, nC, attr.componentCount // nC) def custom_data(self) -> dict[str, Any]: return { diff --git a/src/nd2/readers/protocol.py b/src/nd2/readers/protocol.py index 811dc4d..4c6a1ec 100644 --- a/src/nd2/readers/protocol.py +++ b/src/nd2/readers/protocol.py @@ -10,10 +10,9 @@ from nd2._parse._chunk_decode import get_version if TYPE_CHECKING: - from typing import Any, ContextManager, Mapping, Sequence + from typing import Any, ContextManager, Literal, Mapping, Sequence import numpy as np - from typing_extensions import Literal from nd2._binary import BinaryLayers from nd2._util import FileOrBinaryIO diff --git a/src/nd2/structures.py b/src/nd2/structures.py index 5fd9835..e818cb6 100644 --- a/src/nd2/structures.py +++ b/src/nd2/structures.py @@ -3,9 +3,7 @@ import builtins from dataclasses import dataclass, field from enum import IntEnum -from typing import TYPE_CHECKING, NamedTuple, Union - -from typing_extensions import Literal, TypedDict +from typing import TYPE_CHECKING, Literal, NamedTuple, TypedDict, Union from ._sdk_types import EventMeaning, StimulationType @@ -346,9 +344,9 @@ class Volume: voxelCount: tuple[int, int, int] componentMaxima: list[float] | None = None componentMinima: list[float] | None = None - pixelToStageTransformationMatrix: tuple[ - float, float, float, float, float, float - ] | None = None + pixelToStageTransformationMatrix: ( + tuple[float, float, float, float, float, float] | None + ) = None # NIS Microscope Absolute frame in um = # pixelToStageTransformationMatrix * (X_in_px, Y_in_px, 1) + stagePositionUm diff --git a/tests/test_aicsimage.py b/tests/test_aicsimage.py index d6d3a84..66f1ea8 100644 --- a/tests/test_aicsimage.py +++ b/tests/test_aicsimage.py @@ -3,7 +3,7 @@ It may need updating if it changes upstream """ -import sys + from pathlib import Path from typing import List, Optional, Tuple @@ -17,7 +17,6 @@ DATA = Path(__file__).parent / "data" -@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python3.8 or higher") @pytest.mark.parametrize( ( "filename", diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py deleted file mode 100644 index 0faf367..0000000 --- a/tests/test_deprecations.py +++ /dev/null @@ -1,14 +0,0 @@ -import nd2 -import pytest - - -def test_read_using_sdk(single_nd2): - with pytest.warns(FutureWarning, match="read_using_sdk"): - f = nd2.ND2File(single_nd2, read_using_sdk=True) - f.close() - - -def test_unnest_param(single_nd2): - with nd2.ND2File(single_nd2) as f: - with pytest.warns(FutureWarning, match="unnest"): - f.unstructured_metadata(unnest=True) diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 70ff7ae..932b57b 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -3,7 +3,7 @@ import json import sys from pathlib import Path -from typing import TYPE_CHECKING +from typing import Any, Literal import dask.array as da import pytest @@ -11,16 +11,13 @@ from nd2._parse._chunk_decode import ND2_FILE_SIGNATURE sys.path.append(str(Path(__file__).parent.parent / "scripts")) -from nd2_describe import get_nd2_stats # noqa: E402 +from nd2_describe import get_nd2_stats try: import xarray as xr except ImportError: xr = None -if TYPE_CHECKING: - from typing_extensions import Literal - with open("tests/samples_metadata.json") as f: EXPECTED = json.load(f) @@ -44,7 +41,7 @@ def test_metadata_integrity(path: str) -> None: assert stats[key] == EXPECTED[name][key], f"{key} mismatch" -def _clear_names(*exps): +def _clear_names(*exps: Any) -> None: for exp in exps: for item in exp: if item["type"] == "XYPosLoop": @@ -90,13 +87,11 @@ def test_metadata_extraction(new_nd2: Path) -> None: assert isinstance(nd.unstructured_metadata(), dict) assert isinstance(nd.events(), list) - with pytest.warns(FutureWarning): - assert isinstance(nd.recorded_data, dict) assert nd.closed -def test_metadata_extraction_legacy(old_nd2): +def test_metadata_extraction_legacy(old_nd2: Path) -> None: assert ND2File.is_supported_file(old_nd2) with ND2File(old_nd2) as nd: assert repr(nd) @@ -121,12 +116,11 @@ def test_metadata_extraction_legacy(old_nd2): assert nd.closed -def test_recorded_data() -> None: +def test_events() -> None: # this method is smoke-tested for every file above... # but specific values are asserted here: with ND2File(DATA / "cluster.nd2") as f: - with pytest.warns(FutureWarning, match="deprecated"): - rd = f.recorded_data + rd = f.events(orient="list") headers = list(rd) row_0 = [rd[h][0] for h in headers] @@ -179,7 +173,7 @@ def test_recorded_data() -> None: @pytest.mark.parametrize("orient", ["records", "dict", "list"]) -def test_events(new_nd2: Path, orient: Literal["records", "dict", "list"]) -> None: +def test_events2(new_nd2: Path, orient: Literal["records", "dict", "list"]) -> None: with ND2File(new_nd2) as f: events = f.events(orient=orient)