Skip to content

chore: Resolve CI failures #1413

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

Merged
merged 4 commits into from
Mar 18, 2025
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
25 changes: 25 additions & 0 deletions nibabel/_typing.py
Original file line number Diff line number Diff line change
@@ -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

Check warning on line 18 in nibabel/_typing.py

View check run for this annotation

Codecov / codecov/patch

nibabel/_typing.py#L18

Added line #L18 was not covered by tests


__all__ = [
'ParamSpec',
'Self',
'TypeVar',
]
5 changes: 3 additions & 2 deletions nibabel/arrayproxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
11 changes: 5 additions & 6 deletions nibabel/dataobj_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 2 additions & 6 deletions nibabel/deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
26 changes: 10 additions & 16 deletions nibabel/filebasedimages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion nibabel/gifti/gifti.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
11 changes: 7 additions & 4 deletions nibabel/loadsave.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from __future__ import annotations

import os
import typing as ty

import numpy as np

Expand All @@ -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

Expand Down
7 changes: 1 addition & 6 deletions nibabel/nifti1.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,15 @@
from __future__ import annotations

import json
import sys
import typing as ty
import warnings
from io import BytesIO

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
Expand Down
5 changes: 3 additions & 2 deletions nibabel/openers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions nibabel/pointset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
17 changes: 9 additions & 8 deletions nibabel/spatialimages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions nibabel/volumeutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 '>'
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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'",
Expand Down