Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

build: support python 3.12 #168

Merged
merged 10 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ jobs:
platform: "ubuntu-latest"
- python-version: "3.8"
platform: "windows-latest"
- python-version: "3.12-dev"
platform: "ubuntu-latest"

steps:
- uses: actions/checkout@v4
Expand Down
9 changes: 6 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,35 @@ requires-python = ">=3.7"
license = { text = "BSD 3-Clause License" }
authors = [{ email = "talley.lambert@gmail.com", name = "Talley Lambert" }]
classifiers = [
"Development Status :: 3 - Alpha",
"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",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dynamic = ["version"]
dependencies = ["resource-backed-dask-array", "typing-extensions", "numpy"]

[project.optional-dependencies]
legacy = ["imagecodecs"]
test = [
"aicsimageio",
"aicsimageio; python_version < '3.12'",
"dask[array]",
"imagecodecs",
"lxml",
"numpy >=1.26; python_version >= '3.12'",
"numpy; python_version < '3.12'",
"ome-types",
"psutil",
"pytest-codspeed",
"pytest-cov",
"pytest-pretty",
"pytest>=6.0",
"xarray",
"xarray; python_version < '3.12'",
]
dev = [
"aicsimageio",
Expand Down
10 changes: 3 additions & 7 deletions src/nd2/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import math
import re
import warnings
from datetime import datetime
from datetime import datetime, timezone
from itertools import product
from typing import TYPE_CHECKING, BinaryIO, NamedTuple, cast

Expand Down Expand Up @@ -95,12 +95,8 @@ def is_new_format(path: str) -> bool:
return fh.read(4) == NEW_HEADER_MAGIC


def jdn_to_datetime_local(jdn: float) -> datetime:
return datetime.fromtimestamp((jdn - 2440587.5) * 86400.0)


def jdn_to_datetime_utc(jdn: float) -> datetime:
return datetime.utcfromtimestamp((jdn - 2440587.5) * 86400.0)
def jdn_to_datetime(jdn: float, tz: timezone = timezone.utc) -> datetime:
return datetime.fromtimestamp((jdn - 2440587.5) * 86400.0, tz)


def rgb_int_to_tuple(rgb: int) -> tuple[int, int, int]:
Expand Down
4 changes: 1 addition & 3 deletions src/nd2/readers/_modern/modern_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,9 +545,7 @@ def _acquisition_date(self) -> datetime.datetime | str | None:

time = self._cached_global_metadata().get("time", {})
jdn = time.get("absoluteJulianDayNumber")
if jdn:
return _util.jdn_to_datetime_utc(jdn)
return None
return _util.jdn_to_datetime(jdn) if jdn else None

def binary_data(self) -> BinaryLayers | None:
from nd2._binary import BinaryLayer, BinaryLayers, decode_binary_mask
Expand Down
21 changes: 15 additions & 6 deletions tests/test_events.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
from pathlib import Path

import nd2
import pandas as pd
import numpy as np
import pytest

DATA = Path(__file__).parent / "data"


EXPECTED_COORDS = ([0] * 5 + [1000] * 5 + [2000] * 5) * 3


@pytest.mark.parametrize("fname", ["t3p3z5c3.nd2", "t3p3c3z5.nd2", "t1t1t1p3c3z5.nd2"])
def test_events(fname: str) -> None:
with nd2.ND2File(DATA / fname) as f:
events = f.events()
events = f.events(orient="list")
assert np.array_equal(events["X Coord [µm]"], EXPECTED_COORDS)
assert np.array_equal(events["Y Coord [µm]"], EXPECTED_COORDS)


df = pd.DataFrame(events)
expected_coords = ([0] * 5 + [1000] * 5 + [2000] * 5) * 3
assert all(df["X Coord [µm]"] == expected_coords)
assert all(df["Y Coord [µm]"] == expected_coords)
@pytest.mark.parametrize("fname", ["t3p3z5c3.nd2", "t3p3c3z5.nd2", "t1t1t1p3c3z5.nd2"])
def test_events_pandas(fname: str) -> None:
pd = pytest.importorskip("pandas")
with nd2.ND2File(DATA / fname) as f:
df = pd.DataFrame(f.events())
assert all(df["X Coord [µm]"] == EXPECTED_COORDS)
assert all(df["Y Coord [µm]"] == EXPECTED_COORDS)
assert all(df["Z-Series"] == [-1, -0.5, 0, 0.5, 1] * 9)
assert all(df["T Index"] == [0] * 15 + [1] * 15 + [2] * 15)
assert all(df["Z Index"] == [0, 1, 2, 3, 4] * 9)
Expand Down
13 changes: 9 additions & 4 deletions tests/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@

import dask.array as da
import pytest
import xarray as xr
from nd2 import ND2File, _util, structures
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

try:
import xarray as xr
except ImportError:
xr = None

if TYPE_CHECKING:
from typing_extensions import Literal

Expand Down Expand Up @@ -104,9 +108,10 @@ def test_metadata_extraction_legacy(old_nd2):
assert isinstance(nd.experiment, list)
assert isinstance(nd.text_info, dict)
assert isinstance(nd.metadata, structures.Metadata)
xarr = nd.to_xarray()
assert isinstance(xarr, xr.DataArray)
assert isinstance(xarr.data, da.Array)
if xr is not None:
xarr = nd.to_xarray()
assert isinstance(xarr, xr.DataArray)
assert isinstance(xarr.data, da.Array)

with pytest.warns(UserWarning, match="not implemented"):
nd.events()
Expand Down
5 changes: 4 additions & 1 deletion tests/test_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import dask.array as da
import numpy as np
import pytest
import xarray as xr
from nd2 import ND2File, imread
from nd2._parse._chunk_decode import get_version
from nd2._util import AXIS, is_supported_file
Expand All @@ -23,6 +22,7 @@ def test_read_safety(new_nd2: Path):

def test_position(new_nd2: Path):
"""use position to extract a single stage position with asarray."""
pytest.importorskip("xarray")
if new_nd2.stat().st_size > 250_000_000:
pytest.skip("skipping read on big files")
with ND2File(new_nd2) as nd:
Expand Down Expand Up @@ -53,6 +53,7 @@ def test_dask_closed(single_nd2):


def test_full_read(new_nd2):
pytest.importorskip("xarray")
with ND2File(new_nd2) as nd:
if new_nd2.stat().st_size > 500_000_000:
pytest.skip("skipping full read on big files")
Expand Down Expand Up @@ -82,6 +83,7 @@ def test_full_read_legacy(old_nd2):


def test_xarray(new_nd2):
xr = pytest.importorskip("xarray")
with ND2File(new_nd2) as nd:
xarr = nd.to_xarray()
assert isinstance(xarr, xr.DataArray)
Expand All @@ -90,6 +92,7 @@ def test_xarray(new_nd2):


def test_xarray_legacy(old_nd2):
xr = pytest.importorskip("xarray")
with ND2File(old_nd2) as nd:
xarr = nd.to_xarray()
assert isinstance(xarr, xr.DataArray)
Expand Down
2 changes: 2 additions & 0 deletions tests/test_readme.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

@pytest.mark.skipif(os.name == "nt", reason="paths annoying on windows")
def test_readme(capsys):
pytest.importorskip("xarray")

code = README.read_text().split("```python")[1].split("```")[0]
code = code.replace("some_file.nd2", str(SAMPLE.absolute()))
exec(code)
Expand Down
16 changes: 0 additions & 16 deletions tests/test_segfaults.py

This file was deleted.