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 565a22879..3224376d4 100644 --- a/nibabel/dataobj_images.py +++ b/nibabel/dataobj_images.py @@ -20,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""" @@ -427,12 +426,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 +465,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/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 086e31f12..853c39461 100644 --- a/nibabel/filebasedimages.py +++ b/nibabel/filebasedimages.py @@ -21,15 +21,11 @@ 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 @@ -39,7 +35,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 +49,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 +61,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 +241,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 +356,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 +536,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 +563,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 +594,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/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 35b10c20a..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'] @@ -68,7 +69,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/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 a8e899359..bce17e734 100644 --- a/nibabel/spatialimages.py +++ b/nibabel/spatialimages.py @@ -138,6 +138,7 @@ import numpy as np +from ._typing import TypeVar from .casting import sctypes_aliases from .dataobj_images import DataobjImage from .filebasedimages import FileBasedHeader, FileBasedImage @@ -152,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): @@ -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 diff --git a/nibabel/volumeutils.py b/nibabel/volumeutils.py index cf23d905f..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 '>' @@ -969,7 +971,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, 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'",