diff --git a/.github/workflows/misc.yml b/.github/workflows/misc.yml index 1890488008..ade350aaa7 100644 --- a/.github/workflows/misc.yml +++ b/.github/workflows/misc.yml @@ -24,7 +24,7 @@ jobs: matrix: python-version: ["3.10"] install: ['pip'] - check: ['style', 'doctest'] + check: ['style', 'doctest', 'typing'] pip-flags: [''] depends: ['REQUIREMENTS'] env: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e99b9570d6..addd5f5634 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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/" diff --git a/nibabel/analyze.py b/nibabel/analyze.py index e165112259..fc44693bc6 100644 --- a/nibabel/analyze.py +++ b/nibabel/analyze.py @@ -81,6 +81,9 @@ 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 @@ -88,7 +91,7 @@ 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, @@ -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'), @@ -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 @@ -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 diff --git a/nibabel/benchmarks/bench_arrayproxy_slicing.py b/nibabel/benchmarks/bench_arrayproxy_slicing.py index d313a7db5e..958923d7ea 100644 --- a/nibabel/benchmarks/bench_arrayproxy_slicing.py +++ b/nibabel/benchmarks/bench_arrayproxy_slicing.py @@ -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 diff --git a/nibabel/brikhead.py b/nibabel/brikhead.py index 470ed16664..54b6d021f3 100644 --- a/nibabel/brikhead.py +++ b/nibabel/brikhead.py @@ -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 diff --git a/nibabel/casting.py b/nibabel/casting.py index a17a25a2c8..6232c615b5 100644 --- a/nibabel/casting.py +++ b/nibabel/casting.py @@ -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 @@ -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): diff --git a/nibabel/cmdline/dicomfs.py b/nibabel/cmdline/dicomfs.py index 8de1438544..85d7d8dcad 100644 --- a/nibabel/cmdline/dicomfs.py +++ b/nibabel/cmdline/dicomfs.py @@ -25,7 +25,7 @@ class dummy_fuse: try: - import fuse + import fuse # type: ignore uid = os.getuid() gid = os.getgid() diff --git a/nibabel/ecat.py b/nibabel/ecat.py index de81d8bbe8..8b11e881a7 100644 --- a/nibabel/ecat.py +++ b/nibabel/ecat.py @@ -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 @@ -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 diff --git a/nibabel/externals/netcdf.py b/nibabel/externals/netcdf.py index 2fddcf03d9..b8d1244c0c 100644 --- a/nibabel/externals/netcdf.py +++ b/nibabel/externals/netcdf.py @@ -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. @@ -881,8 +882,8 @@ 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. @@ -890,7 +891,6 @@ def shape(self): same manner of other numpy arrays. """ return self.data.shape - shape = property(shape) def getValue(self): """ diff --git a/nibabel/filebasedimages.py b/nibabel/filebasedimages.py index e37a698f2f..598a735d23 100644 --- a/nibabel/filebasedimages.py +++ b/nibabel/filebasedimages.py @@ -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 @@ -74,7 +76,6 @@ class FileBasedImage: properties: - * shape * header methods: @@ -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 @@ -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 diff --git a/nibabel/freesurfer/mghformat.py b/nibabel/freesurfer/mghformat.py index cb86b4400b..6b97056524 100644 --- a/nibabel/freesurfer/mghformat.py +++ b/nibabel/freesurfer/mghformat.py @@ -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 @@ -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 diff --git a/nibabel/gifti/gifti.py b/nibabel/gifti/gifti.py index c80fbf2e22..919e4faef2 100644 --- a/nibabel/gifti/gifti.py +++ b/nibabel/gifti/gifti.py @@ -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 @@ -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, @@ -832,7 +834,7 @@ 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""" @@ -840,9 +842,12 @@ def to_xml(self, enc='utf-8'): 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 @@ -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): diff --git a/nibabel/minc1.py b/nibabel/minc1.py index fb183277bc..b9d4bc2074 100644 --- a/nibabel/minc1.py +++ b/nibabel/minc1.py @@ -7,8 +7,10 @@ # ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """Read MINC1 format images""" +from __future__ import annotations from numbers import Integral +from typing import Type import numpy as np @@ -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 diff --git a/nibabel/minc2.py b/nibabel/minc2.py index 1fffae0c86..cdb567a996 100644 --- a/nibabel/minc2.py +++ b/nibabel/minc2.py @@ -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: diff --git a/nibabel/nicom/dicomwrappers.py b/nibabel/nicom/dicomwrappers.py index 7e6bea9009..572957f391 100755 --- a/nibabel/nicom/dicomwrappers.py +++ b/nibabel/nicom/dicomwrappers.py @@ -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 diff --git a/nibabel/nifti1.py b/nibabel/nifti1.py index 61a6da3660..9bb88e844c 100644 --- a/nibabel/nifti1.py +++ b/nibabel/nifti1.py @@ -10,8 +10,11 @@ NIfTI1 format defined at http://nifti.nimh.nih.gov/nifti-1/ """ +from __future__ import annotations + import warnings from io import BytesIO +from typing import Type import numpy as np import numpy.linalg as npl @@ -87,8 +90,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 = np.longdouble - _complex256t = np.longcomplex + _float128t: Type[np.generic] = np.longdouble + _complex256t: Type[np.generic] = np.longcomplex else: _float128t = np.void _complex256t = np.void @@ -1814,7 +1817,7 @@ class Nifti1PairHeader(Nifti1Header): class Nifti1Pair(analyze.AnalyzeImage): """Class for NIfTI1 format image, header pair""" - header_class = Nifti1PairHeader + header_class: Type[Nifti1Header] = Nifti1PairHeader _meta_sniff_len = header_class.sizeof_hdr rw = True @@ -1848,9 +1851,7 @@ def __init__(self, dataobj, affine, header=None, extra=None, file_map=None, dtyp self._affine2header() # Copy docstring - __init__.__doc__ = ( - analyze.AnalyzeImage.__init__.__doc__ - + """ + __init__.__doc__ = f"""{analyze.AnalyzeImage.__init__.__doc__} Notes ----- @@ -1863,7 +1864,6 @@ def __init__(self, dataobj, affine, header=None, extra=None, file_map=None, dtyp :meth:`set_qform` methods can be used to update the codes after an image has been created - see those methods, and the :ref:`manual ` for more details. """ - ) def update_header(self): """Harmonize header with image data and affine diff --git a/nibabel/openers.py b/nibabel/openers.py index 4a1b911c95..d75839fe1a 100644 --- a/nibabel/openers.py +++ b/nibabel/openers.py @@ -20,7 +20,7 @@ # is indexed_gzip present and modern? try: - import indexed_gzip as igzip + import indexed_gzip as igzip # type: ignore version = igzip.__version__ diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 27ade56ae9..7c594dcb45 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -1338,7 +1338,7 @@ def from_filename( strict_sort=strict_sort, ) - load = from_filename + load = from_filename # type: ignore -load = PARRECImage.load +load = PARRECImage.from_filename diff --git a/nibabel/pkg_info.py b/nibabel/pkg_info.py index 068600b4e6..73dfd92ed2 100644 --- a/nibabel/pkg_info.py +++ b/nibabel/pkg_info.py @@ -70,7 +70,7 @@ def cmp_pkg_version(version_str: str, pkg_version_str: str = __version__) -> int return _cmp(Version(version_str), Version(pkg_version_str)) -def pkg_commit_hash(pkg_path: str = None) -> tuple[str, str]: +def pkg_commit_hash(pkg_path: str | None = None) -> tuple[str, str]: """Get short form of commit hash In this file is a variable called COMMIT_HASH. This contains a substitution @@ -109,7 +109,7 @@ def pkg_commit_hash(pkg_path: str = None) -> tuple[str, str]: cwd=pkg_path, ) if proc.stdout: - return 'repository', proc.stdout.strip() + return 'repository', proc.stdout.decode().strip() return '(none found)', '' diff --git a/nibabel/pydicom_compat.py b/nibabel/pydicom_compat.py index 9ee2553c5a..4d9df7df7b 100644 --- a/nibabel/pydicom_compat.py +++ b/nibabel/pydicom_compat.py @@ -7,7 +7,7 @@ without error, and always defines: * have_dicom : True if we can import pydicom or dicom; -* pydicom : pydicom module or dicom module or None of not importable; +* pydicom : pydicom module or dicom module or None if not importable; * read_file : ``read_file`` function if pydicom or dicom module is importable else None; * tag_for_keyword : ``tag_for_keyword`` function if pydicom or dicom module @@ -19,26 +19,25 @@ A deprecated copy is available here for backward compatibility. """ +from __future__ import annotations -# Module has (apparently) unused imports; stop flake8 complaining -# flake8: noqa +from typing import Callable from .deprecated import deprecate_with_version +from .optpkg import optional_package -have_dicom = True -pydicom = read_file = tag_for_keyword = Sequence = None +pydicom, have_dicom, _ = optional_package('pydicom') -try: - import pydicom -except ImportError: - have_dicom = False -else: # pydicom module available - # Values not imported by default - import pydicom.values - from pydicom.dicomio import read_file - from pydicom.sequence import Sequence +read_file: Callable | None = None +tag_for_keyword: Callable | None = None +Sequence: type | None = None if have_dicom: + # Values not imported by default + import pydicom.values # type: ignore + from pydicom.dicomio import read_file # noqa:F401 + from pydicom.sequence import Sequence # noqa:F401 + tag_for_keyword = pydicom.datadict.tag_for_keyword diff --git a/nibabel/spatialimages.py b/nibabel/spatialimages.py index 794bb750e6..4bd25e986f 100644 --- a/nibabel/spatialimages.py +++ b/nibabel/spatialimages.py @@ -129,6 +129,9 @@ >>> np.all(img3.get_fdata(dtype=np.float32) == data) True """ +from __future__ import annotations + +from typing import Type import numpy as np @@ -400,7 +403,7 @@ def slice_affine(self, slicer): class SpatialImage(DataobjImage): """Template class for volumetric (3D/4D) images""" - header_class = SpatialHeader + header_class: Type[SpatialHeader] = SpatialHeader ImageSlicer = SpatialFirstSlicer def __init__(self, dataobj, affine, header=None, extra=None, file_map=None): diff --git a/nibabel/spm99analyze.py b/nibabel/spm99analyze.py index 12e3cb658d..a089bedb02 100644 --- a/nibabel/spm99analyze.py +++ b/nibabel/spm99analyze.py @@ -274,7 +274,7 @@ def from_file_map(klass, file_map, *, mmap=True, keep_file_open=None): contents = matf.read() if len(contents) == 0: return ret - import scipy.io as sio + import scipy.io as sio # type: ignore mats = sio.loadmat(BytesIO(contents)) if 'mat' in mats: # this overrides a 'M', and includes any flip diff --git a/nibabel/testing/np_features.py b/nibabel/testing/np_features.py index c0739a8502..eeb783900a 100644 --- a/nibabel/testing/np_features.py +++ b/nibabel/testing/np_features.py @@ -1,24 +1,16 @@ """Look for changes in numpy behavior over versions """ +from functools import lru_cache import numpy as np -def memmap_after_ufunc(): +@lru_cache(maxsize=None) +def memmap_after_ufunc() -> bool: """Return True if ufuncs on memmap arrays always return memmap arrays This should be True for numpy < 1.12, False otherwise. - - Memoize after first call. We do this to avoid having to call this when - importing nibabel.testing, because we cannot depend on the source file - being present - see gh-571. """ - if memmap_after_ufunc.result is not None: - return memmap_after_ufunc.result with open(__file__, 'rb') as fobj: mm_arr = np.memmap(fobj, mode='r', shape=(10,), dtype=np.uint8) - memmap_after_ufunc.result = isinstance(mm_arr + 1, np.memmap) - return memmap_after_ufunc.result - - -memmap_after_ufunc.result = None + return isinstance(mm_arr + 1, np.memmap) diff --git a/nibabel/tmpdirs.py b/nibabel/tmpdirs.py index 5a8eccfa2c..a3be77ffa8 100644 --- a/nibabel/tmpdirs.py +++ b/nibabel/tmpdirs.py @@ -16,7 +16,7 @@ from contextlib import chdir as _chdir except ImportError: # PY310 - @contextmanager + @contextmanager # type: ignore def _chdir(path): cwd = os.getcwd() os.chdir(path) diff --git a/nibabel/volumeutils.py b/nibabel/volumeutils.py index b339b6bab5..225062b2cb 100644 --- a/nibabel/volumeutils.py +++ b/nibabel/volumeutils.py @@ -7,6 +7,7 @@ # ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """Utility functions for analyze-like formats""" +from __future__ import annotations import gzip import sys @@ -29,7 +30,7 @@ native_code = sys_is_le and '<' or '>' swapped_code = sys_is_le and '>' or '<' -endian_codes = ( # numpy code, aliases +_endian_codes = ( # numpy code, aliases ('<', 'little', 'l', 'le', 'L', 'LE'), ('>', 'big', 'BIG', 'b', 'be', 'B', 'BE'), (native_code, 'native', 'n', 'N', '=', '|', 'i', 'I'), @@ -41,7 +42,7 @@ default_compresslevel = 1 #: file-like classes known to hold compressed data -COMPRESSED_FILE_LIKES = (gzip.GzipFile, BZ2File, IndexedGzipFile) +COMPRESSED_FILE_LIKES: tuple[type, ...] = (gzip.GzipFile, BZ2File, IndexedGzipFile) # Enable .zst support if pyzstd installed. if HAVE_ZSTD: @@ -220,7 +221,7 @@ def value_set(self, name=None): # Endian code aliases -endian_codes = Recoder(endian_codes) +endian_codes = Recoder(_endian_codes) class DtypeMapper: diff --git a/nibabel/wrapstruct.py b/nibabel/wrapstruct.py index bf29e0828a..6e236d7356 100644 --- a/nibabel/wrapstruct.py +++ b/nibabel/wrapstruct.py @@ -109,11 +109,13 @@ nib.imageglobals.logger = logger """ +from __future__ import annotations + import numpy as np from . import imageglobals as imageglobals from .batteryrunners import BatteryRunner -from .volumeutils import endian_codes, native_code, pretty_mapping, swapped_code +from .volumeutils import Recoder, endian_codes, native_code, pretty_mapping, swapped_code class WrapStructError(Exception): @@ -482,7 +484,7 @@ def _get_checks(klass): class LabeledWrapStruct(WrapStruct): """A WrapStruct with some fields having value labels for printing etc""" - _field_recoders = {} # for recoding values for str + _field_recoders: dict[str, Recoder] = {} # for recoding values for str def get_value_label(self, fieldname): """Returns label for coded field diff --git a/pyproject.toml b/pyproject.toml index 5a1162a5a6..6d44c607ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,7 @@ test = [ "pytest-httpserver", "pytest-xdist", ] +typing = ["mypy", "pytest", "types-setuptools", "types-Pillow", "pydicom"] zstd = ["pyzstd >= 0.14.3"] [tool.hatch.build.targets.sdist] @@ -102,3 +103,9 @@ force-exclude = """ profile = "black" line_length = 99 extend_skip = ["_version.py", "externals"] + +[tool.mypy] +python_version = "3.11" +exclude = [ + "/tests", +] diff --git a/tools/ci/check.sh b/tools/ci/check.sh index 3cfc1e5530..bcb1a934e2 100755 --- a/tools/ci/check.sh +++ b/tools/ci/check.sh @@ -25,6 +25,8 @@ elif [ "${CHECK_TYPE}" == "test" ]; then cp ../.coveragerc . pytest --doctest-modules --doctest-plus --cov nibabel --cov-report xml \ --junitxml=test-results.xml -v --pyargs nibabel -n auto +elif [ "${CHECK_TYPE}" == "typing" ]; then + mypy nibabel else false fi