Skip to content

Commit 6963164

Browse files
authored
Type checking with mypy (#2655)
* Type checking with mypy The rest of the scientific Python stack doesn't seem to support type annotations yet, but that's OK -- we can use this incrementally in xarray when it seems appropriate, and may check a few bugs. I'm especially excited to use this for internal functions, where we don't always bother with full docstrings (e.g., what is the type of the ``variables`` argument?). This includes: 1. various minor fixes to ensure that "mypy xarray" passes. 2. adding "mypy xarray" to our lint check on Travis-CI. For reference, see "Using mypy with an existing codebase": https://mypy.readthedocs.io/en/stable/existing_code.html Question: are we OK with (2)? This means Travis-CI will fail if your code causes mypy to error. * Lint fix * DOC: document mypy, don't run it in travis * document how to run mypy * fix type annotation * Pin pytest to avoid pytest-cov failure see pytest-dev/pytest-cov#253 * Revert pytest pinning * Revert "Revert pytest pinning" This reverts commit cd187a6. * Revert "Pin pytest to avoid pytest-cov failure" This reverts commit 87ba452.
1 parent ede3e01 commit 6963164

28 files changed

+179
-136
lines changed

ci/requirements-py36.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ dependencies:
1414
- pytest-cov
1515
- pytest-env
1616
- coveralls
17-
- flake8
17+
- pycodestyle
1818
- numpy
1919
- pandas
2020
- scipy
@@ -32,3 +32,4 @@ dependencies:
3232
- lxml
3333
- pip:
3434
- cfgrib>=0.9.2
35+
- mypy==0.650

ci/requirements-py37.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ dependencies:
2929
- pydap
3030
- pip:
3131
- cfgrib>=0.9.2
32+
- mypy==0.650

doc/contributing.rst

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -345,28 +345,34 @@ the more common ``PEP8`` issues:
345345
- passing arguments should have spaces after commas, e.g. ``foo(arg1, arg2, kw1='bar')``
346346

347347
:ref:`Continuous Integration <contributing.ci>` will run
348-
the `flake8 <http://pypi.python.org/pypi/flake8>`_ tool
348+
the `pycodestyle <http://pypi.python.org/pypi/pycodestyle>`_ tool
349349
and report any stylistic errors in your code. Therefore, it is helpful before
350350
submitting code to run the check yourself::
351351

352-
flake8
352+
pycodestyle xarray
353353

354-
If you install `isort <https://github.com/timothycrosley/isort>`_ and
355-
`flake8-isort <https://github.com/gforcada/flake8-isort>`_, this will also show
356-
any errors from incorrectly sorted imports. These aren't currently enforced in
357-
CI. To automatically sort imports, you can run::
354+
Other recommended but optional tools for checking code quality (not currently
355+
enforced in CI):
358356

359-
isort -y
357+
- `mypy <http://mypy-lang.org/>`_ performs static type checking, which can
358+
make it easier to catch bugs. Please run ``mypy xarray`` if you annotate any
359+
code with `type hints <https://docs.python.org/3/library/typing.html>`_.
360+
- `flake8 <http://pypi.python.org/pypi/flake8>`_ includes a few more automated
361+
checks than those enforced by pycodestyle.
362+
- `isort <https://github.com/timothycrosley/isort>`_ will highlight
363+
incorrectly sorted imports. ``isort -y`` will automatically fix them. See
364+
also `flake8-isort <https://github.com/gforcada/flake8-isort>`_.
360365

366+
Note that your code editor probably supports extensions that can show results
367+
of these checks inline as you type.
361368

362369
Backwards Compatibility
363370
~~~~~~~~~~~~~~~~~~~~~~~
364371

365372
Please try to maintain backward compatibility. *xarray* has growing number of users with
366373
lots of existing code, so don't break it if at all possible. If you think breakage is
367374
required, clearly state why as part of the pull request. Also, be careful when changing
368-
method signatures and add deprecation warnings where needed. Also, add the deprecated
369-
sphinx directive to the deprecated functions or methods.
375+
method signatures and add deprecation warnings where needed.
370376

371377
.. _contributing.ci:
372378

setup.cfg

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,64 @@ default_section=THIRDPARTY
2020
known_first_party=xarray
2121
multi_line_output=4
2222

23+
# Most of the numerical computing stack doesn't have type annotations yet.
24+
[mypy-bottleneck.*]
25+
ignore_missing_imports = True
26+
[mypy-cdms2.*]
27+
ignore_missing_imports = True
28+
[mypy-cf_units.*]
29+
ignore_missing_imports = True
30+
[mypy-cfgrib.*]
31+
ignore_missing_imports = True
32+
[mypy-cftime.*]
33+
ignore_missing_imports = True
34+
[mypy-dask.*]
35+
ignore_missing_imports = True
36+
[mypy-distributed.*]
37+
ignore_missing_imports = True
38+
[mypy-h5netcdf.*]
39+
ignore_missing_imports = True
40+
[mypy-h5py.*]
41+
ignore_missing_imports = True
42+
[mypy-iris.*]
43+
ignore_missing_imports = True
44+
[mypy-matplotlib.*]
45+
ignore_missing_imports = True
46+
[mypy-Nio.*]
47+
ignore_missing_imports = True
48+
[mypy-numpy.*]
49+
ignore_missing_imports = True
50+
[mypy-netCDF4.*]
51+
ignore_missing_imports = True
52+
[mypy-netcdftime.*]
53+
ignore_missing_imports = True
54+
[mypy-pandas.*]
55+
ignore_missing_imports = True
56+
[mypy-PseudoNetCDF.*]
57+
ignore_missing_imports = True
58+
[mypy-pydap.*]
59+
ignore_missing_imports = True
60+
[mypy-pytest.*]
61+
ignore_missing_imports = True
62+
[mypy-rasterio.*]
63+
ignore_missing_imports = True
64+
[mypy-scipy.*]
65+
ignore_missing_imports = True
66+
[mypy-seaborn.*]
67+
ignore_missing_imports = True
68+
[mypy-toolz.*]
69+
ignore_missing_imports = True
70+
[mypy-zarr.*]
71+
ignore_missing_imports = True
72+
73+
# written by versioneer
74+
[mypy-xarray._version]
75+
ignore_errors = True
76+
# version spanning code is hard to type annotate (and most of this module will
77+
# be going away soon anyways)
78+
[mypy-xarray.core.pycompat]
79+
ignore_errors = True
80+
2381
[versioneer]
2482
VCS = git
2583
style = pep440

xarray/backends/file_manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import contextlib
22
import threading
3+
from typing import Any, Dict
34
import warnings
45

56
from ..core import utils
@@ -13,7 +14,7 @@
1314
assert FILE_CACHE.maxsize, 'file cache must be at least size one'
1415

1516

16-
REF_COUNTS = {}
17+
REF_COUNTS = {} # type: Dict[Any, int]
1718

1819
_DEFAULT_MODE = utils.ReprObject('<unused>')
1920

xarray/backends/locks.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import multiprocessing
22
import threading
3+
from typing import Any, MutableMapping
34
import weakref
45

56
try:
@@ -20,7 +21,7 @@
2021
NETCDFC_LOCK = SerializableLock()
2122

2223

23-
_FILE_LOCKS = weakref.WeakValueDictionary()
24+
_FILE_LOCKS = weakref.WeakValueDictionary() # type: MutableMapping[Any, threading.Lock] # noqa
2425

2526

2627
def _get_threaded_lock(key):

xarray/coding/cftime_offsets.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import re
4444
from datetime import timedelta
4545
from functools import partial
46+
from typing import ClassVar, Optional
4647

4748
import numpy as np
4849

@@ -74,7 +75,7 @@ def get_date_type(calendar):
7475

7576

7677
class BaseCFTimeOffset(object):
77-
_freq = None
78+
_freq = None # type: ClassVar[str]
7879

7980
def __init__(self, n=1):
8081
if not isinstance(n, int):
@@ -254,9 +255,9 @@ def onOffset(self, date):
254255

255256

256257
class YearOffset(BaseCFTimeOffset):
257-
_freq = None
258-
_day_option = None
259-
_default_month = None
258+
_freq = None # type: ClassVar[str]
259+
_day_option = None # type: ClassVar[str]
260+
_default_month = None # type: ClassVar[int]
260261

261262
def __init__(self, n=1, month=None):
262263
BaseCFTimeOffset.__init__(self, n)

xarray/coding/variables.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Coders for individual Variable objects."""
22
from __future__ import absolute_import, division, print_function
33

4+
from typing import Any
45
import warnings
56
from functools import partial
67

@@ -126,11 +127,12 @@ def pop_to(source, dest, key, name=None):
126127
return value
127128

128129

129-
def _apply_mask(data, # type: np.ndarray
130-
encoded_fill_values, # type: list
131-
decoded_fill_value, # type: Any
132-
dtype, # type: Any
133-
): # type: np.ndarray
130+
def _apply_mask(
131+
data: np.ndarray,
132+
encoded_fill_values: list,
133+
decoded_fill_value: Any,
134+
dtype: Any,
135+
) -> np.ndarray:
134136
"""Mask all matching values in a NumPy arrays."""
135137
data = np.asarray(data, dtype=dtype)
136138
condition = False

xarray/core/alignment.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def _get_joiner(join):
3131
raise ValueError('invalid value for join: %s' % join)
3232

3333

34-
_DEFAULT_EXCLUDE = frozenset()
34+
_DEFAULT_EXCLUDE = frozenset() # type: frozenset
3535

3636

3737
def align(*objects, **kwargs):

xarray/core/common.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def wrapped_func(self, dim=None, axis=None, skipna=None,
2424
return self.reduce(func, dim, axis,
2525
skipna=skipna, allow_lazy=True, **kwargs)
2626
else:
27-
def wrapped_func(self, dim=None, axis=None,
27+
def wrapped_func(self, dim=None, axis=None, # type: ignore
2828
**kwargs):
2929
return self.reduce(func, dim, axis,
3030
allow_lazy=True, **kwargs)
@@ -56,7 +56,7 @@ def wrapped_func(self, dim=None, skipna=None,
5656
numeric_only=numeric_only, allow_lazy=True,
5757
**kwargs)
5858
else:
59-
def wrapped_func(self, dim=None, **kwargs):
59+
def wrapped_func(self, dim=None, **kwargs): # type: ignore
6060
return self.reduce(func, dim,
6161
numeric_only=numeric_only, allow_lazy=True,
6262
**kwargs)

xarray/core/computation.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
import operator
99
from collections import Counter
1010
from distutils.version import LooseVersion
11+
from typing import (
12+
AbstractSet, Any, Dict, Iterable, List, Mapping, Union, Tuple,
13+
TYPE_CHECKING, TypeVar
14+
)
1115

1216
import numpy as np
1317

@@ -16,8 +20,11 @@
1620
from .merge import expand_and_merge_variables
1721
from .pycompat import OrderedDict, basestring, dask_array_type
1822
from .utils import is_dict_like
23+
from .variable import Variable
24+
if TYPE_CHECKING:
25+
from .dataset import Dataset
1926

20-
_DEFAULT_FROZEN_SET = frozenset()
27+
_DEFAULT_FROZEN_SET = frozenset() # type: frozenset
2128
_NO_FILL_VALUE = utils.ReprObject('<no-fill-value>')
2229
_DEFAULT_NAME = utils.ReprObject('<default-name>')
2330
_JOINS_WITHOUT_FILL_VALUES = frozenset({'inner', 'exact'})
@@ -111,8 +118,7 @@ def to_gufunc_string(self):
111118
return str(alt_signature)
112119

113120

114-
def result_name(objects):
115-
# type: List[object] -> Any
121+
def result_name(objects: list) -> Any:
116122
# use the same naming heuristics as pandas:
117123
# https://github.com/blaze/blaze/issues/458#issuecomment-51936356
118124
names = {getattr(obj, 'name', _DEFAULT_NAME) for obj in objects}
@@ -138,10 +144,10 @@ def _get_coord_variables(args):
138144

139145

140146
def build_output_coords(
141-
args, # type: list
142-
signature, # type: _UFuncSignature
143-
exclude_dims=frozenset(), # type: set
144-
):
147+
args: list,
148+
signature: _UFuncSignature,
149+
exclude_dims: AbstractSet = frozenset(),
150+
) -> 'List[OrderedDict[Any, Variable]]':
145151
"""Build output coordinates for an operation.
146152
147153
Parameters
@@ -159,7 +165,6 @@ def build_output_coords(
159165
-------
160166
OrderedDict of Variable objects with merged coordinates.
161167
"""
162-
# type: (...) -> List[OrderedDict[Any, Variable]]
163168
input_coords = _get_coord_variables(args)
164169

165170
if exclude_dims:
@@ -220,17 +225,15 @@ def apply_dataarray_ufunc(func, *args, **kwargs):
220225
return out
221226

222227

223-
def ordered_set_union(all_keys):
224-
# type: List[Iterable] -> Iterable
228+
def ordered_set_union(all_keys: List[Iterable]) -> Iterable:
225229
result_dict = OrderedDict()
226230
for keys in all_keys:
227231
for key in keys:
228232
result_dict[key] = None
229233
return result_dict.keys()
230234

231235

232-
def ordered_set_intersection(all_keys):
233-
# type: List[Iterable] -> Iterable
236+
def ordered_set_intersection(all_keys: List[Iterable]) -> Iterable:
234237
intersection = set(all_keys[0])
235238
for keys in all_keys[1:]:
236239
intersection.intersection_update(keys)
@@ -284,9 +287,9 @@ def _as_variables_or_variable(arg):
284287

285288
def _unpack_dict_tuples(
286289
result_vars, # type: Mapping[Any, Tuple[Variable]]
287-
num_outputs, # type: int
290+
num_outputs, # type: int
288291
):
289-
# type: (...) -> Tuple[Dict[Any, Variable]]
292+
# type: (...) -> Tuple[Dict[Any, Variable], ...]
290293
out = tuple(OrderedDict() for _ in range(num_outputs))
291294
for name, values in result_vars.items():
292295
for value, results_dict in zip(values, out):
@@ -438,8 +441,11 @@ def apply_groupby_ufunc(func, *args):
438441
return combined
439442

440443

441-
def unified_dim_sizes(variables, exclude_dims=frozenset()):
442-
# type: Iterable[Variable] -> OrderedDict[Any, int]
444+
def unified_dim_sizes(
445+
variables: Iterable[Variable],
446+
exclude_dims: AbstractSet = frozenset(),
447+
) -> 'OrderedDict[Any, int]':
448+
443449
dim_sizes = OrderedDict()
444450

445451
for var in variables:
@@ -460,11 +466,9 @@ def unified_dim_sizes(variables, exclude_dims=frozenset()):
460466

461467
SLICE_NONE = slice(None)
462468

463-
# A = TypeVar('A', numpy.ndarray, dask.array.Array)
464-
465469

466470
def broadcast_compat_data(variable, broadcast_dims, core_dims):
467-
# type: (Variable[A], tuple, tuple) -> A
471+
# type: (Variable, tuple, tuple) -> Any
468472
data = variable.data
469473

470474
old_dims = variable.dims

xarray/core/dataarray.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -771,7 +771,8 @@ def __deepcopy__(self, memo=None):
771771
return self.copy(deep=True)
772772

773773
# mutable objects should not be hashable
774-
__hash__ = None
774+
# https://github.com/python/mypy/issues/4266
775+
__hash__ = None # type: ignore
775776

776777
@property
777778
def chunks(self):

0 commit comments

Comments
 (0)