From 7cd34ff397911300f06ad5d120b2db006b98cbee Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Sat, 11 Mar 2023 08:07:09 -0500 Subject: [PATCH 1/3] TYP: Annotate loadsave --- nibabel/imageclasses.py | 10 +++++++--- nibabel/loadsave.py | 42 ++++++++++++++++++++++++----------------- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/nibabel/imageclasses.py b/nibabel/imageclasses.py index e2dbed129..b36131ed9 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 f12b81b30..463a68797 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) From 45cdb1cfddf9332ee13e6340744acb63c1b345e2 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Sat, 25 Mar 2023 22:43:26 -0400 Subject: [PATCH 2/3] TYP: Annotate header types --- nibabel/analyze.py | 1 + nibabel/brikhead.py | 1 + nibabel/cifti2/cifti2.py | 1 + nibabel/ecat.py | 2 +- nibabel/filebasedimages.py | 1 - nibabel/freesurfer/mghformat.py | 1 + nibabel/minc1.py | 1 + nibabel/minc2.py | 1 + nibabel/nifti1.py | 3 ++- nibabel/parrec.py | 1 + nibabel/spatialimages.py | 1 + nibabel/spm2analyze.py | 1 + nibabel/spm99analyze.py | 1 + 13 files changed, 13 insertions(+), 3 deletions(-) diff --git a/nibabel/analyze.py b/nibabel/analyze.py index d738934ff..e4b0455ce 100644 --- a/nibabel/analyze.py +++ b/nibabel/analyze.py @@ -896,6 +896,7 @@ class AnalyzeImage(SpatialImage): """Class for basic Analyze format image""" 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 ee5f76672..6694ff08a 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 423dbfbf9..b41521f0c 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 23a58f752..7f477e4a9 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 3d1a95c1a..daf4e7e0b 100644 --- a/nibabel/filebasedimages.py +++ b/nibabel/filebasedimages.py @@ -159,7 +159,6 @@ class FileBasedImage: """ header_class: Type[FileBasedHeader] = FileBasedHeader - _header: 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 693025efb..5dd266034 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/minc1.py b/nibabel/minc1.py index ebc167b0e..bf3e7e9bb 100644 --- a/nibabel/minc1.py +++ b/nibabel/minc1.py @@ -308,6 +308,7 @@ class Minc1Image(SpatialImage): """ 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 cc0cb5e44..e00608eb2 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 0c824ef6a..71df391d9 100644 --- a/nibabel/nifti1.py +++ b/nibabel/nifti1.py @@ -1817,7 +1817,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 22219382c..ec3fdea71 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 be347bd86..73a5fcf46 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 b326e7eac..fff3ecf08 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 9c2aa15ed..9c5becc6f 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 From 9f189c6d12535c293b5c5911a50fecc6dba473bc Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Sat, 25 Mar 2023 22:44:10 -0400 Subject: [PATCH 3/3] ENH: Drop typing.Type for type --- nibabel/analyze.py | 4 +--- nibabel/filebasedimages.py | 3 +-- nibabel/minc1.py | 3 +-- nibabel/nifti1.py | 5 ++--- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/nibabel/analyze.py b/nibabel/analyze.py index e4b0455ce..20fdac055 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,7 @@ 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')) diff --git a/nibabel/filebasedimages.py b/nibabel/filebasedimages.py index daf4e7e0b..42760cccd 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,7 +157,7 @@ class FileBasedImage: work. """ - header_class: Type[FileBasedHeader] = 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/minc1.py b/nibabel/minc1.py index bf3e7e9bb..5f8422bc2 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,7 @@ 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',) diff --git a/nibabel/nifti1.py b/nibabel/nifti1.py index 71df391d9..07fb17773 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