diff --git a/.github/workflows/run_pytests.yaml b/.github/workflows/run_pytests.yaml index 5e2d82e..6445ef7 100644 --- a/.github/workflows/run_pytests.yaml +++ b/.github/workflows/run_pytests.yaml @@ -27,8 +27,6 @@ jobs: python-version: "3.9" - os: ubuntu-latest python-version: "3.8" - - os: ubuntu-latest - python-version: "3.7" steps: - name: Checkout TiffSlide uses: actions/checkout@v3 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f7e2655..8da9242 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,25 +6,25 @@ repos: - id: end-of-file-fixer - id: check-added-large-files - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 + rev: v3.9.0 hooks: - id: pyupgrade - args: [--py37-plus] + args: [--py38-plus] - repo: https://github.com/pycqa/isort rev: 5.12.0 hooks: - id: isort - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.3.0 hooks: - id: black language_version: python3 - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v1.0.1' + rev: 'v1.4.1' hooks: - id: mypy additional_dependencies: ["numpy"] - exclude: ^examples/ + exclude: ^examples/|^tiffslide/tests/ - repo: https://github.com/PyCQA/flake8 rev: '6.0.0' hooks: @@ -34,7 +34,7 @@ repos: language_version: python3 exclude: "^(build|docs|setup.py)|tests[/]" - repo: https://github.com/PyCQA/bandit - rev: '1.7.4' + rev: '1.7.5' hooks: - id: bandit args: ["--ini", ".bandit", "-lll"] diff --git a/setup.cfg b/setup.cfg index 8146f60..5b0415d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,11 +15,11 @@ classifiers = License :: OSI Approved :: BSD License Programming Language :: Python 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 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Topic :: Scientific/Engineering Topic :: Scientific/Engineering :: Information Analysis Topic :: Scientific/Engineering :: Bio-Informatics @@ -32,7 +32,7 @@ classifiers = [options] packages = find: -python_requires = >=3.7 +python_requires = >=3.8 install_requires = imagecodecs fsspec!=2022.11.0,!=2023.1.0 diff --git a/tiffslide/_compat.py b/tiffslide/_compat.py index 9afe8a4..ad699d6 100644 --- a/tiffslide/_compat.py +++ b/tiffslide/_compat.py @@ -13,14 +13,10 @@ from types import TracebackType from typing import TYPE_CHECKING from typing import Any +from typing import Literal from typing import Mapping from typing import Sequence -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal - import numpy as np import zarr from imagecodecs import __version__ as _imagecodecs_version diff --git a/tiffslide/_kerchunk.py b/tiffslide/_kerchunk.py index 9a0d66e..e5b9ba0 100644 --- a/tiffslide/_kerchunk.py +++ b/tiffslide/_kerchunk.py @@ -13,11 +13,7 @@ from io import StringIO from typing import TYPE_CHECKING from typing import Any - -if sys.version_info >= (3, 8): - from typing import TypedDict -else: - from typing_extensions import TypedDict +from typing import TypedDict import fsspec from imagecodecs.numcodecs import register_codecs diff --git a/tiffslide/_pycompat.py b/tiffslide/_pycompat.py deleted file mode 100644 index f1c79ab..0000000 --- a/tiffslide/_pycompat.py +++ /dev/null @@ -1,151 +0,0 @@ -"""python3.7 compatibility code - -This will be removed as soon as we drop python 3.7 - -""" -from __future__ import annotations - -import re -import sys -from threading import RLock -from typing import Any -from typing import Callable -from typing import Generic -from typing import Iterator -from typing import Mapping -from typing import TypeVar - -import zarr -from tifffile import __version__ as tifffile_version - -__all__ = [ - "REQUIRES_STORE_FIX", - "py37_fix_store", - "cached_property", -] - - -def _requires_store_fix(ver_zarr: str, ver_tifffile: str) -> bool: - _v_regex = re.compile(r"^([0-9]+)[.]([0-9]+)[.]([0-9]+)(a|b|rc)?([0-9]+)?$") - _v_map = {"a": 0, "b": 1, "rc": 2, None: 99} - - mz = _v_regex.match(ver_zarr) - mt = _v_regex.match(ver_tifffile) - if mz is None or mt is None: - return True - - _ver_zarr: list[int] = [ - int(x) if x and x.isdigit() else _v_map[x] for x in mz.groups() - ] - _ver_tifffile: list[int] = [ - int(x) if x and x.isdigit() else _v_map[x] for x in mt.groups() - ] - - _new_zarr = _ver_zarr >= [2, 11, 0, 99, 99] - _old_tifffile = _ver_tifffile < [2022, 3, 29, 99, 99] - return _new_zarr and _old_tifffile - - -REQUIRES_STORE_FIX = _requires_store_fix(zarr.__version__, tifffile_version) - - -# --- zarr - tifffile compatibility patches --------------------------- -# -# note: we can drop this once we drop python 3.7 - - -class _IncompatibleStoreShim(Mapping[str, Any]): - """ - A compatibility shim, for python=3.7 - with zarr>=2.11.0 with tifffile<2022.3.29 - """ - - def __init__(self, mapping: Mapping[str, Any]) -> None: - self._mutable_mapping = mapping - - def __getitem__(self, key: str) -> Any: - if key.endswith((".zarray", ".zgroup")) and key not in self._mutable_mapping: - raise KeyError(key) - try: - return self._mutable_mapping[key] - except ValueError: - raise KeyError(key) - - def __iter__(self) -> Iterator[str]: - return iter(self._mutable_mapping) - - def __len__(self) -> int: - return len(self._mutable_mapping) - - def __getattr__(self, item: str) -> Any: - return getattr(self._mutable_mapping, item) - - -def py37_fix_store(zstore: Mapping[str, Any]) -> Mapping[str, Any]: - """python 3.7 compatibility fix for tifffile and zarr""" - if REQUIRES_STORE_FIX: - return _IncompatibleStoreShim(zstore) - else: - return zstore - - -if sys.version_info < (3, 8): - # --- vendored cached_property from CPython with added type information --- - - _T = TypeVar("_T") - _NOT_FOUND = object() - - class cached_property(Generic[_T]): - def __init__(self, func: Callable[..., _T]) -> None: - self.func = func - self.attrname = None - self.__doc__ = func.__doc__ - self.lock = RLock() - - def __set_name__(self, owner, name): # type: ignore - if self.attrname is None: - self.attrname = name - elif name != self.attrname: - raise TypeError( - "Cannot assign the same cached_property to two different names " - f"({self.attrname!r} and {name!r})." - ) - - def __get__(self, instance: Any, owner: type[Any] | None = None) -> _T: - if instance is None: - return self # type: ignore - if self.attrname is None: - raise TypeError( - "Cannot use cached_property instance without calling __set_name__ on it." - ) - try: - cache = instance.__dict__ - except ( - AttributeError - ): # not all objects have __dict__ (e.g. class defines slots) - msg = ( - f"No '__dict__' attribute on {type(instance).__name__!r} " - f"instance to cache {self.attrname!r} property." - ) - raise TypeError(msg) from None - val = cache.get(self.attrname, _NOT_FOUND) - if val is _NOT_FOUND: - with self.lock: - # check if another thread filled cache while we awaited lock - val = cache.get(self.attrname, _NOT_FOUND) - if val is _NOT_FOUND: - val = self.func(instance) - try: - cache[self.attrname] = val - except TypeError: - msg = ( - f"The '__dict__' attribute on {type(instance).__name__!r} instance " - f"does not support item assignment for caching {self.attrname!r} property." - ) - raise TypeError(msg) from None - return val - - # __class_getitem__ = classmethod(GenericAlias) - -else: - from functools import cached_property diff --git a/tiffslide/_types.py b/tiffslide/_types.py index 542ee7b..663c661 100644 --- a/tiffslide/_types.py +++ b/tiffslide/_types.py @@ -6,17 +6,10 @@ from typing import IO from typing import TYPE_CHECKING from typing import Any +from typing import Protocol +from typing import TypedDict from typing import Union - -if sys.version_info >= (3, 8): - from typing import Protocol - from typing import TypedDict - from typing import runtime_checkable - -else: - from typing_extensions import Protocol - from typing_extensions import TypedDict - from typing_extensions import runtime_checkable +from typing import runtime_checkable if sys.version_info >= (3, 10): from typing import TypeAlias diff --git a/tiffslide/_zarr.py b/tiffslide/_zarr.py index 104ef5b..758b5a7 100644 --- a/tiffslide/_zarr.py +++ b/tiffslide/_zarr.py @@ -4,12 +4,10 @@ from __future__ import annotations import json -import sys from typing import TYPE_CHECKING from typing import Any from typing import Iterator from typing import Mapping -from warnings import warn import numpy as np import zarr @@ -19,8 +17,6 @@ from zarr.storage import FSStore from tiffslide._compat import NotTiffFile -from tiffslide._pycompat import REQUIRES_STORE_FIX -from tiffslide._pycompat import py37_fix_store from tiffslide._types import Point3D from tiffslide._types import SeriesCompositionInfo from tiffslide._types import Size3D @@ -42,12 +38,6 @@ "get_zarr_selection", ] -if REQUIRES_STORE_FIX and sys.version_info >= (3, 8): - warn( - "detected outdated tifffile version on `python>=3.8` with `zarr>=2.11.0`: " - "updating tifffile is recommended!" - ) - # --- zarr storage classes -------------------------------------------- @@ -120,8 +110,6 @@ def _get_series_zarr( zstore = FSStore(f"s{series_idx}", fs=obj) else: raise NotImplementedError(f"{type(obj).__name__} unsupported") - if REQUIRES_STORE_FIX: - zstore = py37_fix_store(zstore) return zstore # type: ignore @@ -302,7 +290,7 @@ def get_zarr_chunk_sizes( except AttributeError: raise RuntimeError("probably not supported with your tifffile version") - chunk_sizes = np.full(chunked, dtype=np.int64, fill_value=-1) + chunk_sizes: NDArray[np.int64] = np.full(chunked, dtype=np.int64, fill_value=-1) # _index = "" for indices in np.ndindex(*chunked): diff --git a/tiffslide/tests/conftest.py b/tiffslide/tests/conftest.py index 1053229..4429ada 100644 --- a/tiffslide/tests/conftest.py +++ b/tiffslide/tests/conftest.py @@ -179,13 +179,10 @@ def gen_im(size_hw): mpp = 0.25 mag = 40 filename = "ASD" - if sys.version_info >= (3, 8): - resolution_kw = { - "resolution": (10000 / mpp, 10000 / mpp), - "resolutionunit": "CENTIMETER", - } - else: - resolution_kw = {"resolution": (10000 / mpp, 10000 / mpp, "CENTIMETER")} + resolution_kw = { + "resolution": (10000 / mpp, 10000 / mpp), + "resolutionunit": "CENTIMETER", + } # write to svs format with tifffile.TiffWriter(pth, bigtiff=True) as tif: diff --git a/tiffslide/tests/test_pycompat.py b/tiffslide/tests/test_pycompat.py deleted file mode 100644 index fa02b12..0000000 --- a/tiffslide/tests/test_pycompat.py +++ /dev/null @@ -1,17 +0,0 @@ -import pytest - -from tiffslide._pycompat import _requires_store_fix - - -@pytest.mark.parametrize( - "vz,vt,fix", - [ - ("2.11.0", "2022.3.29rc7", True), - ("2.10.1a3", "2022.3.29", False), - ("2.12.0", "2022.4.0", False), - ("2.13.0rc1", "2022.12.0", False), - ("2.11.3b1", "2022.2.11", True), - ], -) -def test_requires_fix(vz, vt, fix): - assert _requires_store_fix(vz, vt) == fix diff --git a/tiffslide/tiffslide.py b/tiffslide/tiffslide.py index 60ea2df..6120a98 100644 --- a/tiffslide/tiffslide.py +++ b/tiffslide/tiffslide.py @@ -5,28 +5,19 @@ import sys from collections import defaultdict from fractions import Fraction +from functools import cached_property from itertools import count from types import TracebackType from typing import TYPE_CHECKING from typing import Any from typing import Iterator +from typing import Literal from typing import Mapping from typing import TypeVar from typing import overload from warnings import warn from xml.etree import ElementTree -from tifffile import TiffPage - -from tiffslide._types import Slice3D - -if sys.version_info[:2] >= (3, 8): - from functools import cached_property - from typing import Literal -else: - from tiffslide._pycompat import cached_property - from typing_extensions import Literal - import numpy as np import tifffile import zarr @@ -36,6 +27,7 @@ from PIL import Image from tifffile import TiffFile from tifffile import TiffFileError as TiffFileError +from tifffile import TiffPage from tifffile import TiffPageSeries from tifffile.tifffile import svs_description_metadata @@ -43,6 +35,7 @@ from tiffslide._types import OpenFileLike from tiffslide._types import PathOrFileOrBufferLike from tiffslide._types import SeriesCompositionInfo +from tiffslide._types import Slice3D from tiffslide._types import TiffFileIO from tiffslide._zarr import get_zarr_depth_and_dtype from tiffslide._zarr import get_zarr_selection