Skip to content

Commit

Permalink
Merge pull request #1115 from nipy/enh/type_annotations
Browse files Browse the repository at this point in the history
ENH: Add minimal type annotations and run mypy in CI
  • Loading branch information
effigies authored Jan 6, 2023
2 parents faf4566 + 85f4cb4 commit a60d2b7
Show file tree
Hide file tree
Showing 28 changed files with 113 additions and 105 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/misc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
matrix:
python-version: ["3.10"]
install: ['pip']
check: ['style', 'doctest']
check: ['style', 'doctest', 'typing']
pip-flags: ['']
depends: ['REQUIREMENTS']
env:
Expand Down
12 changes: 12 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,15 @@ repos:
hooks:
- id: flake8
exclude: "^(doc|nisext|tools)/"
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.991
hooks:
- id: mypy
# Sync with project.optional-dependencies.typing
additional_dependencies:
- pytest
- types-setuptools
- types-Pillow
- pydicom
# Sync with tool.mypy['exclude']
exclude: "^(doc|nisext|tools)/|.*/tests/"
17 changes: 10 additions & 7 deletions nibabel/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,17 @@
can be loaded with and without a default flip, so the saved zoom will not
constrain the affine.
"""
from __future__ import annotations

from typing import Type

import numpy as np

from .arrayproxy import ArrayProxy
from .arraywriters import ArrayWriter, WriterError, get_slope_inter, make_array_writer
from .batteryrunners import Report
from .fileholders import copy_file_map
from .spatialimages import HeaderDataError, HeaderTypeError, SpatialImage
from .spatialimages import HeaderDataError, HeaderTypeError, SpatialHeader, SpatialImage
from .volumeutils import (
apply_read_scaling,
array_from_file,
Expand Down Expand Up @@ -131,7 +134,7 @@
('glmax', 'i4'),
('glmin', 'i4'),
]
data_history_dtd = [
data_history_dtd: list[tuple[str, str] | tuple[str, str, tuple[int, ...]]] = [
('descrip', 'S80'),
('aux_file', 'S24'),
('orient', 'S1'),
Expand Down Expand Up @@ -172,7 +175,7 @@
data_type_codes = make_dt_codes(_dtdefs)


class AnalyzeHeader(LabeledWrapStruct):
class AnalyzeHeader(LabeledWrapStruct, SpatialHeader):
"""Class for basic analyze header
Implements zoom-only setting of affine transform, and no image
Expand Down Expand Up @@ -892,11 +895,11 @@ def may_contain_header(klass, binaryblock):
class AnalyzeImage(SpatialImage):
"""Class for basic Analyze format image"""

header_class = AnalyzeHeader
header_class: Type[AnalyzeHeader] = AnalyzeHeader
_meta_sniff_len = header_class.sizeof_hdr
files_types = (('image', '.img'), ('header', '.hdr'))
valid_exts = ('.img', '.hdr')
_compressed_suffixes = ('.gz', '.bz2', '.zst')
files_types: tuple[tuple[str, str], ...] = (('image', '.img'), ('header', '.hdr'))
valid_exts: tuple[str, ...] = ('.img', '.hdr')
_compressed_suffixes: tuple[str, ...] = ('.gz', '.bz2', '.zst')

makeable = True
rw = True
Expand Down
2 changes: 1 addition & 1 deletion nibabel/benchmarks/bench_arrayproxy_slicing.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

# if memory_profiler is installed, we get memory usage results
try:
from memory_profiler import memory_usage
from memory_profiler import memory_usage # type: ignore
except ImportError:
memory_usage = None

Expand Down
1 change: 0 additions & 1 deletion nibabel/brikhead.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
am aware) always be >= 1. This permits sub-brick indexing common in AFNI
programs (e.g., example4d+orig'[0]').
"""

import os
import re
from copy import deepcopy
Expand Down
3 changes: 2 additions & 1 deletion nibabel/casting.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Most routines work round some numpy oddities in floating point precision and
casting. Others work round numpy casting to and from python ints
"""
from __future__ import annotations

import warnings
from numbers import Integral
Expand Down Expand Up @@ -110,7 +111,7 @@ def float_to_int(arr, int_type, nan2zero=True, infmax=False):


# Cache range values
_SHARED_RANGES = {}
_SHARED_RANGES: dict[tuple[type, type], tuple[np.number, np.number]] = {}


def shared_range(flt_type, int_type):
Expand Down
2 changes: 1 addition & 1 deletion nibabel/cmdline/dicomfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class dummy_fuse:


try:
import fuse
import fuse # type: ignore

uid = os.getuid()
gid = os.getgid()
Expand Down
4 changes: 2 additions & 2 deletions nibabel/ecat.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@

from .arraywriters import make_array_writer
from .fileslice import canonical_slicers, predict_shape, slice2outax
from .spatialimages import SpatialImage
from .spatialimages import SpatialHeader, SpatialImage
from .volumeutils import array_from_file, make_dt_codes, native_code, swapped_code
from .wrapstruct import WrapStruct

Expand Down Expand Up @@ -243,7 +243,7 @@
patient_orient_neurological = [1, 3, 5, 7]


class EcatHeader(WrapStruct):
class EcatHeader(WrapStruct, SpatialHeader):
"""Class for basic Ecat PET header
Sub-parts of standard Ecat File
Expand Down
4 changes: 2 additions & 2 deletions nibabel/externals/netcdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,7 @@ def __setattr__(self, attr, value):
pass
self.__dict__[attr] = value

@property
def isrec(self):
"""Returns whether the variable has a record dimension or not.
Expand All @@ -881,16 +882,15 @@ def isrec(self):
"""
return bool(self.data.shape) and not self._shape[0]
isrec = property(isrec)

@property
def shape(self):
"""Returns the shape tuple of the data variable.
This is a read-only attribute and can not be modified in the
same manner of other numpy arrays.
"""
return self.data.shape
shape = property(shape)

def getValue(self):
"""
Expand Down
38 changes: 10 additions & 28 deletions nibabel/filebasedimages.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
#
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
"""Common interface for any image format--volume or surface, binary or xml."""
from __future__ import annotations

import io
from copy import deepcopy
from typing import Type
from urllib import request

from .fileholders import FileHolder
Expand Down Expand Up @@ -74,7 +76,6 @@ class FileBasedImage:
properties:
* shape
* header
methods:
Expand Down Expand Up @@ -118,25 +119,6 @@ class FileBasedImage:
img.to_file_map()
You can get the data out again with::
img.get_fdata()
Less commonly, for some image types that support it, you might want to
fetch out the unscaled array via the object containing the data::
unscaled_data = img.dataoobj.get_unscaled()
Analyze-type images (including nifti) support this, but others may not
(MINC, for example).
Sometimes you might to avoid any loss of precision by making the
data type the same as the input::
hdr = img.header
hdr.set_data_dtype(data.dtype)
img.to_filename(fname)
**Files interface**
The image has an attribute ``file_map``. This is a mapping, that has keys
Expand All @@ -158,20 +140,20 @@ class FileBasedImage:
contain enough information so that an existing image instance can save
itself back to the files pointed to in ``file_map``. When a file holder
holds active file-like objects, then these may be affected by the
initial file read; in this case, the contains file-like objects need to
initial file read; in this case, the file-like objects need to
carry the position at which a write (with ``to_file_map``) should place the
data. The ``file_map`` contents should therefore be such, that this will
work.
"""

header_class = FileBasedHeader
_meta_sniff_len = 0
files_types = (('image', None),)
valid_exts = ()
_compressed_suffixes = ()
header_class: Type[FileBasedHeader] = FileBasedHeader
_meta_sniff_len: int = 0
files_types: tuple[tuple[str, str | None], ...] = (('image', None),)
valid_exts: tuple[str, ...] = ()
_compressed_suffixes: tuple[str, ...] = ()

makeable = True # Used in test code
rw = True # Used in test code
makeable: bool = True # Used in test code
rw: bool = True # Used in test code

def __init__(self, header=None, extra=None, file_map=None):
"""Initialize image
Expand Down
4 changes: 2 additions & 2 deletions nibabel/freesurfer/mghformat.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from ..fileholders import FileHolder
from ..filename_parser import _stringify_path
from ..openers import ImageOpener
from ..spatialimages import HeaderDataError, SpatialImage
from ..spatialimages import HeaderDataError, SpatialHeader, SpatialImage
from ..volumeutils import Recoder, array_from_file, array_to_file, endian_codes
from ..wrapstruct import LabeledWrapStruct

Expand Down Expand Up @@ -87,7 +87,7 @@ class MGHError(Exception):
"""


class MGHHeader(LabeledWrapStruct):
class MGHHeader(LabeledWrapStruct, SpatialHeader):
"""Class for MGH format header
The header also consists of the footer data which MGH places after the data
Expand Down
15 changes: 10 additions & 5 deletions nibabel/gifti/gifti.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
The Gifti specification was (at time of writing) available as a PDF download
from http://www.nitrc.org/projects/gifti/
"""
from __future__ import annotations

import base64
import sys
import warnings
from typing import Type

import numpy as np

Expand Down Expand Up @@ -577,7 +579,7 @@ class GiftiImage(xml.XmlSerializable, SerializableImage):
# The parser will in due course be a GiftiImageParser, but we can't set
# that now, because it would result in a circular import. We set it after
# the class has been defined, at the end of the class definition.
parser = None
parser: Type[xml.XmlParser]

def __init__(
self,
Expand Down Expand Up @@ -832,17 +834,20 @@ def _to_xml_element(self):
GIFTI.append(dar._to_xml_element())
return GIFTI

def to_xml(self, enc='utf-8'):
def to_xml(self, enc='utf-8') -> bytes:
"""Return XML corresponding to image content"""
header = b"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE GIFTI SYSTEM "http://www.nitrc.org/frs/download.php/115/gifti.dtd">
"""
return header + super().to_xml(enc)

# Avoid the indirection of going through to_file_map
to_bytes = to_xml
def to_bytes(self, enc='utf-8'):
return self.to_xml(enc=enc)

def to_file_map(self, file_map=None):
to_bytes.__doc__ = SerializableImage.to_bytes.__doc__

def to_file_map(self, file_map=None, enc='utf-8'):
"""Save the current image to the specified file_map
Parameters
Expand All @@ -858,7 +863,7 @@ def to_file_map(self, file_map=None):
if file_map is None:
file_map = self.file_map
with file_map['image'].get_prepare_fileobj('wb') as f:
f.write(self.to_xml())
f.write(self.to_xml(enc=enc))

@classmethod
def from_file_map(klass, file_map, buffer_size=35000000, mmap=True):
Expand Down
12 changes: 7 additions & 5 deletions nibabel/minc1.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
#
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
"""Read MINC1 format images"""
from __future__ import annotations

from numbers import Integral
from typing import Type

import numpy as np

Expand Down Expand Up @@ -305,11 +307,11 @@ class Minc1Image(SpatialImage):
load.
"""

header_class = Minc1Header
_meta_sniff_len = 4
valid_exts = ('.mnc',)
files_types = (('image', '.mnc'),)
_compressed_suffixes = ('.gz', '.bz2', '.zst')
header_class: Type[MincHeader] = Minc1Header
_meta_sniff_len: int = 4
valid_exts: tuple[str, ...] = ('.mnc',)
files_types: tuple[tuple[str, str], ...] = (('image', '.mnc'),)
_compressed_suffixes: tuple[str, ...] = ('.gz', '.bz2', '.zst')

makeable = True
rw = False
Expand Down
2 changes: 1 addition & 1 deletion nibabel/minc2.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ class Minc2Image(Minc1Image):
def from_file_map(klass, file_map, *, mmap=True, keep_file_open=None):
# Import of h5py might take awhile for MPI-enabled builds
# So we are importing it here "on demand"
import h5py
import h5py # type: ignore

holder = file_map['image']
if holder.filename is None:
Expand Down
2 changes: 0 additions & 2 deletions nibabel/nicom/dicomwrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,6 @@ class Wrapper:
is_multiframe = False
b_matrix = None
q_vector = None
b_value = None
b_vector = None

def __init__(self, dcm_data):
"""Initialize wrapper
Expand Down
Loading

0 comments on commit a60d2b7

Please sign in to comment.