From 71a792b0ae4818802cc82ccccd5f0021b90eac63 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 18 Mar 2025 10:59:40 -0400 Subject: [PATCH 1/4] chore: Bump numpy to avoid setuptools rug-pull --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 73f01b66e..b6b420c79 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ readme = "README.rst" license = { text = "MIT License" } requires-python = ">=3.9" dependencies = [ - "numpy >=1.22", + "numpy >=1.23", "packaging >=20", "importlib_resources >=5.12; python_version < '3.12'", "typing_extensions >=4.6; python_version < '3.13'", From efb32944691d981e9a0f2083fda9abbb75a489fa Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 18 Mar 2025 11:01:36 -0400 Subject: [PATCH 2/4] sty: Apply ruff fix for Self-returning methods --- nibabel/dataobj_images.py | 9 +++++---- nibabel/filebasedimages.py | 22 +++++++++++----------- nibabel/gifti/gifti.py | 2 +- nibabel/openers.py | 2 +- nibabel/spatialimages.py | 13 +++++++------ 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/nibabel/dataobj_images.py b/nibabel/dataobj_images.py index 565a22879..0c12468c1 100644 --- a/nibabel/dataobj_images.py +++ b/nibabel/dataobj_images.py @@ -13,6 +13,7 @@ import typing as ty import numpy as np +from typing_extensions import Self from .deprecated import deprecate_with_version from .filebasedimages import FileBasedHeader, FileBasedImage @@ -427,12 +428,12 @@ def ndim(self) -> int: @classmethod def from_file_map( - klass: type[ArrayImgT], + klass, file_map: FileMap, *, mmap: bool | ty.Literal['c', 'r'] = True, keep_file_open: bool | None = None, - ) -> ArrayImgT: + ) -> Self: """Class method to create image from mapping in ``file_map`` Parameters @@ -466,12 +467,12 @@ def from_file_map( @classmethod def from_filename( - klass: type[ArrayImgT], + klass, filename: FileSpec, *, mmap: bool | ty.Literal['c', 'r'] = True, keep_file_open: bool | None = None, - ) -> ArrayImgT: + ) -> Self: """Class method to create image from filename `filename` Parameters diff --git a/nibabel/filebasedimages.py b/nibabel/filebasedimages.py index 086e31f12..1fe15418e 100644 --- a/nibabel/filebasedimages.py +++ b/nibabel/filebasedimages.py @@ -15,6 +15,8 @@ from copy import deepcopy from urllib import request +from typing_extensions import Self + from ._compression import COMPRESSION_ERRORS from .fileholders import FileHolder, FileMap from .filename_parser import TypesFilenamesError, _stringify_path, splitext_addext, types_filenames @@ -39,7 +41,7 @@ class FileBasedHeader: """Template class to implement header protocol""" @classmethod - def from_header(klass: type[HdrT], header: FileBasedHeader | ty.Mapping | None = None) -> HdrT: + def from_header(klass, header: FileBasedHeader | ty.Mapping | None = None) -> Self: if header is None: return klass() # I can't do isinstance here because it is not necessarily true @@ -53,7 +55,7 @@ def from_header(klass: type[HdrT], header: FileBasedHeader | ty.Mapping | None = ) @classmethod - def from_fileobj(klass: type[HdrT], fileobj: io.IOBase) -> HdrT: + def from_fileobj(klass, fileobj: io.IOBase) -> Self: raise NotImplementedError def write_to(self, fileobj: io.IOBase) -> None: @@ -65,7 +67,7 @@ def __eq__(self, other: object) -> bool: def __ne__(self, other: object) -> bool: return not self == other - def copy(self: HdrT) -> HdrT: + def copy(self) -> Self: """Copy object to independent representation The copy should not be affected by any changes to the original @@ -245,12 +247,12 @@ def set_filename(self, filename: str) -> None: self.file_map = self.__class__.filespec_to_file_map(filename) @classmethod - def from_filename(klass: type[ImgT], filename: FileSpec) -> ImgT: + def from_filename(klass, filename: FileSpec) -> Self: file_map = klass.filespec_to_file_map(filename) return klass.from_file_map(file_map) @classmethod - def from_file_map(klass: type[ImgT], file_map: FileMap) -> ImgT: + def from_file_map(klass, file_map: FileMap) -> Self: raise NotImplementedError @classmethod @@ -360,7 +362,7 @@ def instance_to_filename(klass, img: FileBasedImage, filename: FileSpec) -> None img.to_filename(filename) @classmethod - def from_image(klass: type[ImgT], img: FileBasedImage) -> ImgT: + def from_image(klass, img: FileBasedImage) -> Self: """Class method to create new instance of own class from `img` Parameters @@ -540,7 +542,7 @@ def _filemap_from_iobase(klass, io_obj: io.IOBase) -> FileMap: return klass.make_file_map({klass.files_types[0][0]: io_obj}) @classmethod - def from_stream(klass: type[StreamImgT], io_obj: io.IOBase) -> StreamImgT: + def from_stream(klass, io_obj: io.IOBase) -> Self: """Load image from readable IO stream Convert to BytesIO to enable seeking, if input stream is not seekable @@ -567,7 +569,7 @@ def to_stream(self, io_obj: io.IOBase, **kwargs) -> None: self.to_file_map(self._filemap_from_iobase(io_obj), **kwargs) @classmethod - def from_bytes(klass: type[StreamImgT], bytestring: bytes) -> StreamImgT: + def from_bytes(klass, bytestring: bytes) -> Self: """Construct image from a byte string Class method @@ -598,9 +600,7 @@ def to_bytes(self, **kwargs) -> bytes: return bio.getvalue() @classmethod - def from_url( - klass: type[StreamImgT], url: str | request.Request, timeout: float = 5 - ) -> StreamImgT: + def from_url(klass, url: str | request.Request, timeout: float = 5) -> Self: """Retrieve and load an image from a URL Class method diff --git a/nibabel/gifti/gifti.py b/nibabel/gifti/gifti.py index 76fcc4a45..ff7a9bdde 100644 --- a/nibabel/gifti/gifti.py +++ b/nibabel/gifti/gifti.py @@ -867,7 +867,7 @@ def to_xml(self, enc='utf-8', *, mode='strict', **kwargs) -> bytes: if arr.datatype not in GIFTI_DTYPES: arr = copy(arr) # TODO: Better typing for recoders - dtype = cast(np.dtype, data_type_codes.dtype[arr.datatype]) + dtype = cast('np.dtype', data_type_codes.dtype[arr.datatype]) if np.issubdtype(dtype, np.floating): arr.datatype = data_type_codes['float32'] elif np.issubdtype(dtype, np.integer): diff --git a/nibabel/openers.py b/nibabel/openers.py index 35b10c20a..029315e21 100644 --- a/nibabel/openers.py +++ b/nibabel/openers.py @@ -68,7 +68,7 @@ def __init__( if filename is None: raise TypeError('Must define either fileobj or filename') # Cast because GzipFile.myfileobj has type io.FileIO while open returns ty.IO - fileobj = self.myfileobj = ty.cast(io.FileIO, open(filename, modestr)) + fileobj = self.myfileobj = ty.cast('io.FileIO', open(filename, modestr)) super().__init__( filename='', mode=modestr, diff --git a/nibabel/spatialimages.py b/nibabel/spatialimages.py index a8e899359..636e1d95c 100644 --- a/nibabel/spatialimages.py +++ b/nibabel/spatialimages.py @@ -137,6 +137,7 @@ from typing import Literal import numpy as np +from typing_extensions import Self from .casting import sctypes_aliases from .dataobj_images import DataobjImage @@ -203,9 +204,9 @@ def __init__( @classmethod def from_header( - klass: type[SpatialHdrT], + klass, header: SpatialProtocol | FileBasedHeader | ty.Mapping | None = None, - ) -> SpatialHdrT: + ) -> Self: if header is None: return klass() # I can't do isinstance here because it is not necessarily true @@ -227,7 +228,7 @@ def __eq__(self, other: object) -> bool: ) return NotImplemented - def copy(self: SpatialHdrT) -> SpatialHdrT: + def copy(self) -> Self: """Copy object to independent representation The copy should not be affected by any changes to the original @@ -586,7 +587,7 @@ def set_data_dtype(self, dtype: npt.DTypeLike) -> None: self._header.set_data_dtype(dtype) @classmethod - def from_image(klass: type[SpatialImgT], img: SpatialImage | FileBasedImage) -> SpatialImgT: + def from_image(klass, img: SpatialImage | FileBasedImage) -> Self: """Class method to create new instance of own class from `img` Parameters @@ -610,7 +611,7 @@ def from_image(klass: type[SpatialImgT], img: SpatialImage | FileBasedImage) -> return super().from_image(img) @property - def slicer(self: SpatialImgT) -> SpatialFirstSlicer[SpatialImgT]: + def slicer(self) -> SpatialFirstSlicer[Self]: """Slicer object that returns cropped and subsampled images The image is resliced in the current orientation; no rotation or @@ -658,7 +659,7 @@ def orthoview(self) -> OrthoSlicer3D: """ return OrthoSlicer3D(self.dataobj, self.affine, title=self.get_filename()) - def as_reoriented(self: SpatialImgT, ornt: Sequence[Sequence[int]]) -> SpatialImgT: + def as_reoriented(self, ornt: Sequence[Sequence[int]]) -> Self: """Apply an orientation change and return a new image If ornt is identity transform, return the original image, unchanged From 6b14f843ac3f28cc82a276bb305ecda3f29d70f4 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 18 Mar 2025 11:04:52 -0400 Subject: [PATCH 3/4] typ: Improve argument type for volumeutils.int_scinter_ftype --- nibabel/volumeutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nibabel/volumeutils.py b/nibabel/volumeutils.py index cf23d905f..cd6d7b4f5 100644 --- a/nibabel/volumeutils.py +++ b/nibabel/volumeutils.py @@ -969,7 +969,7 @@ def working_type( def int_scinter_ftype( - ifmt: type[np.integer], + ifmt: np.dtype[np.integer] | type[np.integer], slope: npt.ArrayLike = 1.0, inter: npt.ArrayLike = 0.0, default: type[np.floating] = np.float32, From 694862506354daaf6b37643516e8699eddb3e2c0 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 18 Mar 2025 11:31:13 -0400 Subject: [PATCH 4/4] typ: Consolidate typing_extensions imports --- nibabel/_typing.py | 25 +++++++++++++++++++++++++ nibabel/arrayproxy.py | 5 +++-- nibabel/dataobj_images.py | 4 +--- nibabel/deprecated.py | 8 ++------ nibabel/filebasedimages.py | 8 +------- nibabel/loadsave.py | 11 +++++++---- nibabel/nifti1.py | 7 +------ nibabel/openers.py | 3 ++- nibabel/pointset.py | 4 ++-- nibabel/spatialimages.py | 6 +++--- nibabel/volumeutils.py | 8 +++++--- 11 files changed, 52 insertions(+), 37 deletions(-) create mode 100644 nibabel/_typing.py diff --git a/nibabel/_typing.py b/nibabel/_typing.py new file mode 100644 index 000000000..8b6203181 --- /dev/null +++ b/nibabel/_typing.py @@ -0,0 +1,25 @@ +"""Helpers for typing compatibility across Python versions""" + +import sys + +if sys.version_info < (3, 10): + from typing_extensions import ParamSpec +else: + from typing import ParamSpec + +if sys.version_info < (3, 11): + from typing_extensions import Self +else: + from typing import Self + +if sys.version_info < (3, 13): + from typing_extensions import TypeVar +else: + from typing import TypeVar + + +__all__ = [ + 'ParamSpec', + 'Self', + 'TypeVar', +] diff --git a/nibabel/arrayproxy.py b/nibabel/arrayproxy.py index ed2310519..82713f639 100644 --- a/nibabel/arrayproxy.py +++ b/nibabel/arrayproxy.py @@ -59,10 +59,11 @@ if ty.TYPE_CHECKING: import numpy.typing as npt - from typing_extensions import Self # PY310 + + from ._typing import Self, TypeVar # Taken from numpy/__init__.pyi - _DType = ty.TypeVar('_DType', bound=np.dtype[ty.Any]) + _DType = TypeVar('_DType', bound=np.dtype[ty.Any]) class ArrayLike(ty.Protocol): diff --git a/nibabel/dataobj_images.py b/nibabel/dataobj_images.py index 0c12468c1..3224376d4 100644 --- a/nibabel/dataobj_images.py +++ b/nibabel/dataobj_images.py @@ -13,7 +13,6 @@ import typing as ty import numpy as np -from typing_extensions import Self from .deprecated import deprecate_with_version from .filebasedimages import FileBasedHeader, FileBasedImage @@ -21,12 +20,11 @@ if ty.TYPE_CHECKING: import numpy.typing as npt + from ._typing import Self from .arrayproxy import ArrayLike from .fileholders import FileMap from .filename_parser import FileSpec -ArrayImgT = ty.TypeVar('ArrayImgT', bound='DataobjImage') - class DataobjImage(FileBasedImage): """Template class for images that have dataobj data stores""" diff --git a/nibabel/deprecated.py b/nibabel/deprecated.py index d39c0624d..394fb0799 100644 --- a/nibabel/deprecated.py +++ b/nibabel/deprecated.py @@ -5,15 +5,11 @@ import typing as ty import warnings +from ._typing import ParamSpec from .deprecator import Deprecator from .pkg_info import cmp_pkg_version -if ty.TYPE_CHECKING: - # PY39: ParamSpec is available in Python 3.10+ - P = ty.ParamSpec('P') -else: - # Just to keep the runtime happy - P = ty.TypeVar('P') +P = ParamSpec('P') class ModuleProxy: diff --git a/nibabel/filebasedimages.py b/nibabel/filebasedimages.py index 1fe15418e..853c39461 100644 --- a/nibabel/filebasedimages.py +++ b/nibabel/filebasedimages.py @@ -15,23 +15,17 @@ from copy import deepcopy from urllib import request -from typing_extensions import Self - from ._compression import COMPRESSION_ERRORS from .fileholders import FileHolder, FileMap from .filename_parser import TypesFilenamesError, _stringify_path, splitext_addext, types_filenames from .openers import ImageOpener if ty.TYPE_CHECKING: + from ._typing import Self from .filename_parser import ExtensionSpec, FileSpec FileSniff = tuple[bytes, str] -ImgT = ty.TypeVar('ImgT', bound='FileBasedImage') -HdrT = ty.TypeVar('HdrT', bound='FileBasedHeader') - -StreamImgT = ty.TypeVar('StreamImgT', bound='SerializableImage') - class ImageFileError(Exception): pass diff --git a/nibabel/loadsave.py b/nibabel/loadsave.py index e39aeceba..e398092ab 100644 --- a/nibabel/loadsave.py +++ b/nibabel/loadsave.py @@ -12,7 +12,6 @@ from __future__ import annotations import os -import typing as ty import numpy as np @@ -26,13 +25,17 @@ _compressed_suffixes = ('.gz', '.bz2', '.zst') -if ty.TYPE_CHECKING: +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import TypedDict + + from ._typing import ParamSpec from .filebasedimages import FileBasedImage from .filename_parser import FileSpec - P = ty.ParamSpec('P') + P = ParamSpec('P') - class Signature(ty.TypedDict): + class Signature(TypedDict): signature: bytes format_name: str diff --git a/nibabel/nifti1.py b/nibabel/nifti1.py index 5ea3041fc..e39f9f904 100644 --- a/nibabel/nifti1.py +++ b/nibabel/nifti1.py @@ -14,7 +14,6 @@ from __future__ import annotations import json -import sys import typing as ty import warnings from io import BytesIO @@ -22,12 +21,8 @@ import numpy as np import numpy.linalg as npl -if sys.version_info < (3, 13): - from typing_extensions import Self, TypeVar # PY312 -else: - from typing import Self, TypeVar - from . import analyze # module import +from ._typing import Self, TypeVar from .arrayproxy import get_obj_dtype from .batteryrunners import Report from .casting import have_binary128 diff --git a/nibabel/openers.py b/nibabel/openers.py index 029315e21..2d95d4813 100644 --- a/nibabel/openers.py +++ b/nibabel/openers.py @@ -22,7 +22,8 @@ from types import TracebackType from _typeshed import WriteableBuffer - from typing_extensions import Self + + from ._typing import Self ModeRT = ty.Literal['r', 'rt'] ModeRB = ty.Literal['rb'] diff --git a/nibabel/pointset.py b/nibabel/pointset.py index 759a0b15e..1d20b82fe 100644 --- a/nibabel/pointset.py +++ b/nibabel/pointset.py @@ -31,9 +31,9 @@ from nibabel.spatialimages import SpatialImage if ty.TYPE_CHECKING: - from typing_extensions import Self + from ._typing import Self, TypeVar - _DType = ty.TypeVar('_DType', bound=np.dtype[ty.Any]) + _DType = TypeVar('_DType', bound=np.dtype[ty.Any]) class CoordinateArray(ty.Protocol): diff --git a/nibabel/spatialimages.py b/nibabel/spatialimages.py index 636e1d95c..bce17e734 100644 --- a/nibabel/spatialimages.py +++ b/nibabel/spatialimages.py @@ -137,8 +137,8 @@ from typing import Literal import numpy as np -from typing_extensions import Self +from ._typing import TypeVar from .casting import sctypes_aliases from .dataobj_images import DataobjImage from .filebasedimages import FileBasedHeader, FileBasedImage @@ -153,11 +153,11 @@ import numpy.typing as npt + from ._typing import Self from .arrayproxy import ArrayLike from .fileholders import FileMap -SpatialImgT = ty.TypeVar('SpatialImgT', bound='SpatialImage') -SpatialHdrT = ty.TypeVar('SpatialHdrT', bound='SpatialHeader') +SpatialImgT = TypeVar('SpatialImgT', bound='SpatialImage') class HasDtype(ty.Protocol): diff --git a/nibabel/volumeutils.py b/nibabel/volumeutils.py index cd6d7b4f5..41bff7275 100644 --- a/nibabel/volumeutils.py +++ b/nibabel/volumeutils.py @@ -28,11 +28,13 @@ import numpy.typing as npt + from ._typing import TypeVar + Scalar = np.number | float - K = ty.TypeVar('K') - V = ty.TypeVar('V') - DT = ty.TypeVar('DT', bound=np.generic) + K = TypeVar('K') + V = TypeVar('V') + DT = TypeVar('DT', bound=np.generic) sys_is_le = sys.byteorder == 'little' native_code: ty.Literal['<', '>'] = '<' if sys_is_le else '>'