diff --git a/nibabel/analyze.py b/nibabel/analyze.py index d738934fff..20fdac055a 100644 --- a/nibabel/analyze.py +++ b/nibabel/analyze.py @@ -83,8 +83,6 @@ """ from __future__ import annotations -from typing import Type - import numpy as np from .arrayproxy import ArrayProxy @@ -895,7 +893,8 @@ def may_contain_header(klass, binaryblock): class AnalyzeImage(SpatialImage): """Class for basic Analyze format image""" - header_class: Type[AnalyzeHeader] = AnalyzeHeader + header_class: type[AnalyzeHeader] = AnalyzeHeader + header: AnalyzeHeader _meta_sniff_len = header_class.sizeof_hdr files_types: tuple[tuple[str, str], ...] = (('image', '.img'), ('header', '.hdr')) valid_exts: tuple[str, ...] = ('.img', '.hdr') diff --git a/nibabel/brikhead.py b/nibabel/brikhead.py index ee5f766722..6694ff08a5 100644 --- a/nibabel/brikhead.py +++ b/nibabel/brikhead.py @@ -475,6 +475,7 @@ class AFNIImage(SpatialImage): """ header_class = AFNIHeader + header: AFNIHeader valid_exts = ('.brik', '.head') files_types = (('image', '.brik'), ('header', '.head')) _compressed_suffixes = ('.gz', '.bz2', '.Z', '.zst') diff --git a/nibabel/cifti2/cifti2.py b/nibabel/cifti2/cifti2.py index 423dbfbf9d..b41521f0cd 100644 --- a/nibabel/cifti2/cifti2.py +++ b/nibabel/cifti2/cifti2.py @@ -1411,6 +1411,7 @@ class Cifti2Image(DataobjImage, SerializableImage): """Class for single file CIFTI-2 format image""" header_class = Cifti2Header + header: Cifti2Header valid_exts = Nifti2Image.valid_exts files_types = Nifti2Image.files_types makeable = False diff --git a/nibabel/ecat.py b/nibabel/ecat.py index 23a58f752e..7f477e4a97 100644 --- a/nibabel/ecat.py +++ b/nibabel/ecat.py @@ -751,7 +751,7 @@ class EcatImage(SpatialImage): valid_exts = ('.v',) files_types = (('image', '.v'), ('header', '.v')) - _header: EcatHeader + header: EcatHeader _subheader: EcatSubHeader ImageArrayProxy = EcatImageArrayProxy diff --git a/nibabel/filebasedimages.py b/nibabel/filebasedimages.py index 3d1a95c1a4..42760cccdf 100644 --- a/nibabel/filebasedimages.py +++ b/nibabel/filebasedimages.py @@ -12,7 +12,6 @@ import io import typing as ty from copy import deepcopy -from typing import Type from urllib import request from ._compression import COMPRESSION_ERRORS @@ -158,8 +157,7 @@ class FileBasedImage: work. """ - header_class: Type[FileBasedHeader] = FileBasedHeader - _header: FileBasedHeader + header_class: type[FileBasedHeader] = FileBasedHeader _meta_sniff_len: int = 0 files_types: tuple[ExtensionSpec, ...] = (('image', None),) valid_exts: tuple[str, ...] = () diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index 693025efbe..5dd2660342 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -462,6 +462,7 @@ class MGHImage(SpatialImage, SerializableImage): """Class for MGH format image""" header_class = MGHHeader + header: MGHHeader valid_exts = ('.mgh', '.mgz') # Register that .mgz extension signals gzip compression ImageOpener.compress_ext_map['.mgz'] = ImageOpener.gz_def diff --git a/nibabel/imageclasses.py b/nibabel/imageclasses.py index e2dbed129d..b36131ed94 100644 --- a/nibabel/imageclasses.py +++ b/nibabel/imageclasses.py @@ -7,9 +7,13 @@ # ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """Define supported image classes and names""" +from __future__ import annotations + from .analyze import AnalyzeImage from .brikhead import AFNIImage from .cifti2 import Cifti2Image +from .dataobj_images import DataobjImage +from .filebasedimages import FileBasedImage from .freesurfer import MGHImage from .gifti import GiftiImage from .minc1 import Minc1Image @@ -21,7 +25,7 @@ from .spm99analyze import Spm99AnalyzeImage # Ordered by the load/save priority. -all_image_classes = [ +all_image_classes: list[type[FileBasedImage]] = [ Nifti1Pair, Nifti1Image, Nifti2Pair, @@ -41,7 +45,7 @@ # Image classes known to require spatial axes to be first in index ordering. # When adding an image class, consider whether the new class should be listed # here. -KNOWN_SPATIAL_FIRST = ( +KNOWN_SPATIAL_FIRST: tuple[type[FileBasedImage], ...] = ( Nifti1Pair, Nifti1Image, Nifti2Pair, @@ -55,7 +59,7 @@ ) -def spatial_axes_first(img): +def spatial_axes_first(img: DataobjImage) -> bool: """True if spatial image axes for `img` always precede other axes Parameters diff --git a/nibabel/loadsave.py b/nibabel/loadsave.py index f12b81b30b..463a687975 100644 --- a/nibabel/loadsave.py +++ b/nibabel/loadsave.py @@ -8,7 +8,10 @@ ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## # module imports """Utilities to load and save image objects""" +from __future__ import annotations + import os +import typing as ty import numpy as np @@ -22,7 +25,18 @@ _compressed_suffixes = ('.gz', '.bz2', '.zst') -def _signature_matches_extension(filename): +if ty.TYPE_CHECKING: # pragma: no cover + from .filebasedimages import FileBasedImage + from .filename_parser import FileSpec + + P = ty.ParamSpec('P') + + class Signature(ty.TypedDict): + signature: bytes + format_name: str + + +def _signature_matches_extension(filename: FileSpec) -> tuple[bool, str]: """Check if signature aka magic number matches filename extension. Parameters @@ -42,7 +56,7 @@ def _signature_matches_extension(filename): the empty string otherwise. """ - signatures = { + signatures: dict[str, Signature] = { '.gz': {'signature': b'\x1f\x8b', 'format_name': 'gzip'}, '.bz2': {'signature': b'BZh', 'format_name': 'bzip2'}, '.zst': {'signature': b'\x28\xb5\x2f\xfd', 'format_name': 'ztsd'}, @@ -64,7 +78,7 @@ def _signature_matches_extension(filename): return False, f'File {filename} is not a {format_name} file' -def load(filename, **kwargs): +def load(filename: FileSpec, **kwargs) -> FileBasedImage: r"""Load file given filename, guessing at file type Parameters @@ -126,7 +140,7 @@ def guessed_image_type(filename): raise ImageFileError(f'Cannot work out file type of "{filename}"') -def save(img, filename, **kwargs): +def save(img: FileBasedImage, filename: FileSpec, **kwargs) -> None: r"""Save an image to file adapting format to `filename` Parameters @@ -161,19 +175,17 @@ def save(img, filename, **kwargs): from .nifti1 import Nifti1Image, Nifti1Pair from .nifti2 import Nifti2Image, Nifti2Pair - klass = None - converted = None - + converted: FileBasedImage if type(img) == Nifti1Image and lext in ('.img', '.hdr'): - klass = Nifti1Pair + converted = Nifti1Pair.from_image(img) elif type(img) == Nifti2Image and lext in ('.img', '.hdr'): - klass = Nifti2Pair + converted = Nifti2Pair.from_image(img) elif type(img) == Nifti1Pair and lext == '.nii': - klass = Nifti1Image + converted = Nifti1Image.from_image(img) elif type(img) == Nifti2Pair and lext == '.nii': - klass = Nifti2Image + converted = Nifti2Image.from_image(img) else: # arbitrary conversion - valid_klasses = [klass for klass in all_image_classes if ext in klass.valid_exts] + valid_klasses = [klass for klass in all_image_classes if lext in klass.valid_exts] if not valid_klasses: # if list is empty raise ImageFileError(f'Cannot work out file type of "{filename}"') @@ -186,13 +198,9 @@ def save(img, filename, **kwargs): break except Exception as e: err = e - # ... and if none of them work, raise an error. - if converted is None: + else: raise err - # Here, we either have a klass or a converted image. - if converted is None: - converted = klass.from_image(img) converted.to_filename(filename, **kwargs) diff --git a/nibabel/minc1.py b/nibabel/minc1.py index ebc167b0ee..5f8422bc23 100644 --- a/nibabel/minc1.py +++ b/nibabel/minc1.py @@ -10,7 +10,6 @@ from __future__ import annotations from numbers import Integral -from typing import Type import numpy as np @@ -307,7 +306,8 @@ class Minc1Image(SpatialImage): load. """ - header_class: Type[MincHeader] = Minc1Header + header_class: type[MincHeader] = Minc1Header + header: MincHeader _meta_sniff_len: int = 4 valid_exts: tuple[str, ...] = ('.mnc',) files_types: tuple[tuple[str, str], ...] = (('image', '.mnc'),) diff --git a/nibabel/minc2.py b/nibabel/minc2.py index cc0cb5e440..e00608eb2f 100644 --- a/nibabel/minc2.py +++ b/nibabel/minc2.py @@ -150,6 +150,7 @@ class Minc2Image(Minc1Image): # MINC2 does not do compressed whole files _compressed_suffixes = () header_class = Minc2Header + header: Minc2Header @classmethod def from_file_map(klass, file_map, *, mmap=True, keep_file_open=None): diff --git a/nibabel/nifti1.py b/nibabel/nifti1.py index 0c824ef6ad..07fb177736 100644 --- a/nibabel/nifti1.py +++ b/nibabel/nifti1.py @@ -14,7 +14,6 @@ import warnings from io import BytesIO -from typing import Type import numpy as np import numpy.linalg as npl @@ -90,8 +89,8 @@ # datatypes not in analyze format, with codes if have_binary128(): # Only enable 128 bit floats if we really have IEEE binary 128 longdoubles - _float128t: Type[np.generic] = np.longdouble - _complex256t: Type[np.generic] = np.longcomplex + _float128t: type[np.generic] = np.longdouble + _complex256t: type[np.generic] = np.longcomplex else: _float128t = np.void _complex256t = np.void @@ -1817,7 +1816,8 @@ class Nifti1PairHeader(Nifti1Header): class Nifti1Pair(analyze.AnalyzeImage): """Class for NIfTI1 format image, header pair""" - header_class: Type[Nifti1Header] = Nifti1PairHeader + header_class: type[Nifti1Header] = Nifti1PairHeader + header: Nifti1Header _meta_sniff_len = header_class.sizeof_hdr rw = True diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 22219382c8..ec3fdea711 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -1253,6 +1253,7 @@ class PARRECImage(SpatialImage): """PAR/REC image""" header_class = PARRECHeader + header: PARRECHeader valid_exts = ('.rec', '.par') files_types = (('image', '.rec'), ('header', '.par')) diff --git a/nibabel/spatialimages.py b/nibabel/spatialimages.py index be347bd86f..73a5fcf468 100644 --- a/nibabel/spatialimages.py +++ b/nibabel/spatialimages.py @@ -476,6 +476,7 @@ class SpatialImage(DataobjImage): ImageSlicer: type[SpatialFirstSlicer] = SpatialFirstSlicer _header: SpatialHeader + header: SpatialHeader def __init__( self, diff --git a/nibabel/spm2analyze.py b/nibabel/spm2analyze.py index b326e7eac0..fff3ecf086 100644 --- a/nibabel/spm2analyze.py +++ b/nibabel/spm2analyze.py @@ -128,6 +128,7 @@ class Spm2AnalyzeImage(spm99.Spm99AnalyzeImage): """Class for SPM2 variant of basic Analyze image""" header_class = Spm2AnalyzeHeader + header: Spm2AnalyzeHeader load = Spm2AnalyzeImage.from_filename diff --git a/nibabel/spm99analyze.py b/nibabel/spm99analyze.py index 9c2aa15ed0..9c5becc6f6 100644 --- a/nibabel/spm99analyze.py +++ b/nibabel/spm99analyze.py @@ -227,6 +227,7 @@ class Spm99AnalyzeImage(analyze.AnalyzeImage): """Class for SPM99 variant of basic Analyze image""" header_class = Spm99AnalyzeHeader + header: Spm99AnalyzeHeader files_types = (('image', '.img'), ('header', '.hdr'), ('mat', '.mat')) has_affine = True makeable = True