Skip to content

Commit b95e82d

Browse files
authored
Merge pull request #81 from StagPython/type_annotations
Improve type soundness This PR adds static type checking with `mypy`. It also comes with a few API changes to make it simpler/more correct wrt to types: * use of `MappingProxyType` instead of `OrderedDict` * new `datatypes` module to hold `Var*` as well as `Field`, `Rprof`, and `Tseries` classes * those classes are now `NamedTuple`s instead of bare `namedtuple`s * `processing` functions now return `Field`, `Rprof` and `Tseries` instances directly * new `NoGeomError`, `NoRefstateError` and `NoTimeError` exceptions to signal missing data instead of a plain `None` * new parsing function `field_header` and `field_istep`. The `fields` function always return the full data instead of having a return type depending on logical arguments * successive calls to `_StepsView.filter` on the same instance now compose instead of overriding
2 parents 0cf28f4 + a6f5bc3 commit b95e82d

29 files changed

+1741
-1413
lines changed

.lgtm.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
queries:
2+
- exclude: py/import-and-import-from

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Welcome to StagPy's documentation!
4242
sources/apiref/args
4343
sources/apiref/commands
4444
sources/apiref/config
45+
sources/apiref/datatypes
4546
sources/apiref/error
4647
sources/apiref/field
4748
sources/apiref/parfile

docs/sources/apiref/datatypes.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
datatypes
2+
=========
3+
4+
.. automodule:: stagpy.datatypes
5+
6+
.. autoclass:: Varf
7+
.. autoclass:: Field
8+
.. autoclass:: Varr
9+
.. autoclass:: Rprof
10+
.. autoclass:: Vart
11+
.. autoclass:: Tseries

docs/sources/apiref/phyvars.rst

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,6 @@ phyvars
33

44
.. automodule:: stagpy.phyvars
55

6-
.. class:: Varf
7-
8-
:class:`collections.namedtuple` whose instances hold metadata of
9-
scalar fields. It defines the following fields:
10-
11-
- **description** (*str* or *func*): short description of the variable if
12-
it is output by StagYY, function to compute it otherwise.
13-
- **dim** (*str*): dimension used to
14-
:func:`~stagpy.stagyydata.StagyyData.scale` to dimensional values.
15-
166
.. data:: FIELD
177
:annotation: = {fieldvar: Varf()}
188

@@ -31,18 +21,6 @@ phyvars
3121
Dictionary of surface scalar fields output by StagYY. Keys are the
3222
variable names, values are :class:`Varf` instances.
3323

34-
.. class:: Varr
35-
36-
:class:`collections.namedtuple` whose instances hold metadata of
37-
radial profiles. It defines the following fields:
38-
39-
- **description** (*str* or *func*): short description of the variable if
40-
it is output by StagYY, function to compute it otherwise.
41-
- **kind** (*str*): shorter description to group similar variables under
42-
the same label.
43-
- **dim** (*str*): dimension used to
44-
:func:`~stagpy.stagyydata.StagyyData.scale` to dimensional values.
45-
4624
.. data:: RPROF
4725
:annotation: = {rprofvar: Varr()}
4826

@@ -61,18 +39,6 @@ phyvars
6139
Dictionary of radial profiles of the reference state. Keys are the
6240
variable names, values are :class:`Varr` instances.
6341

64-
.. class:: Vart
65-
66-
:class:`collections.namedtuple` whose instances hold metadata of
67-
time series. It defines the following fields:
68-
69-
- **description** (*str* or *func*): short description of the variable if
70-
it is output by StagYY, function to compute it otherwise.
71-
- **kind** (*str*): shorter description to group similar variables under
72-
the same label.
73-
- **dim** (*str*): dimension used to
74-
:func:`~stagpy.stagyydata.StagyyData.scale` to dimensional values.
75-
7642
.. data:: TIME
7743
:annotation: = {timevar: Vart()}
7844

docs/sources/apiref/step.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ _step
44
.. automodule:: stagpy._step
55
:members:
66
:private-members:
7-
:exclude-members: _init_shape, _get_raw_data, _scale_radius_mo
7+
:exclude-members: _init_shape, _get_raw_data, _scale_radius_mo,
8+
_present_fields

pyproject.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,18 @@ requires = ["setuptools>=45", "setuptools_scm>=6.2", "wheel"]
33
build-backend = "setuptools.build_meta"
44

55
[tool.setuptools_scm]
6+
7+
[tool.mypy]
8+
check_untyped_defs = true
9+
10+
[[tool.mypy.overrides]]
11+
module = [
12+
"setuptools_scm",
13+
"f90nml.*",
14+
"h5py.*",
15+
"matplotlib.*",
16+
"mpl_toolkits.*",
17+
"pandas.*",
18+
"scipy.*",
19+
]
20+
ignore_missing_imports = true

stagpy/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
and uppercase versions of those.
1717
"""
1818

19-
import importlib
19+
from __future__ import annotations
2020
import os
2121
import pathlib
2222
import shutil
@@ -30,7 +30,7 @@
3030
from . import config
3131

3232

33-
def _env(var):
33+
def _env(var: str) -> bool:
3434
"""Return whether var is set to True."""
3535
val = os.getenv(var, default='').lower()
3636
return val in ('true', 't', 'yes', 'y', 'on', '1')
@@ -69,7 +69,7 @@ def _check_config():
6969

7070
def load_mplstyle():
7171
"""Try to load conf.plot.mplstyle matplotlib style."""
72-
plt = importlib.import_module('matplotlib.pyplot')
72+
import matplotlib.pyplot as plt
7373
if conf.plot.mplstyle:
7474
for style in conf.plot.mplstyle.split():
7575
found = False

stagpy/__main__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""The stagpy module is callable."""
22

3-
import importlib
43
import signal
54
import sys
65
import warnings
@@ -13,8 +12,7 @@ def main():
1312
if not DEBUG:
1413
signal.signal(signal.SIGINT, sigint_handler)
1514
warnings.simplefilter('ignore')
16-
args = importlib.import_module('stagpy.args')
17-
error = importlib.import_module('stagpy.error')
15+
from . import args, error
1816
try:
1917
args.parse_args()()
2018
except error.StagpyError as err:

stagpy/_helpers.py

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,31 @@
11
"""Various helper functions and classes."""
22

3+
from __future__ import annotations
34
from inspect import getdoc
5+
from typing import TYPE_CHECKING, Generic, TypeVar
46

57
import matplotlib.pyplot as plt
68

79
from . import conf
810

11+
if TYPE_CHECKING:
12+
from typing import Optional, Any, List, Callable
13+
from matplotlib.figure import Figure
14+
from numpy import ndarray
915

10-
def out_name(stem, timestep=None):
16+
17+
def out_name(stem: str, timestep: Optional[int] = None) -> str:
1118
"""Return StagPy out file name.
1219
1320
Args:
14-
stem (str): short description of file content.
15-
timestep (int): timestep if relevant.
21+
stem: short description of file content.
22+
timestep: timestep if relevant.
1623
1724
Returns:
18-
str: the output file name.
25+
the output file name.
1926
2027
Other Parameters:
21-
conf.core.outname (str): the generic name stem, defaults to
22-
``'stagpy'``.
28+
conf.core.outname: the generic name stem, defaults to ``'stagpy'``.
2329
"""
2430
if conf.core.shortname:
2531
return conf.core.outname
@@ -28,32 +34,33 @@ def out_name(stem, timestep=None):
2834
return conf.core.outname + '_' + stem
2935

3036

31-
def scilabel(value, precision=2):
37+
def scilabel(value: float, precision: int = 2) -> str:
3238
"""Build scientific notation of some value.
3339
3440
This is dedicated to use in labels displaying scientific values.
3541
3642
Args:
37-
value (float): numeric value to format.
38-
precision (int): number of decimal digits.
43+
value: numeric value to format.
44+
precision: number of decimal digits.
3945
4046
Returns:
41-
str: the scientific notation the specified value.
47+
the scientific notation of the specified value.
4248
"""
43-
man, exp = f'{value:.{precision}e}'.split('e')
44-
exp = int(exp)
49+
man, exps = f'{value:.{precision}e}'.split('e')
50+
exp = int(exps)
4551
return fr'{man}\times 10^{{{exp}}}'
4652

4753

48-
def saveplot(fig, *name_args, close=True, **name_kwargs):
54+
def saveplot(fig: Figure, *name_args: Any, close: bool = True,
55+
**name_kwargs: Any):
4956
"""Save matplotlib figure.
5057
5158
You need to provide :data:`stem` as a positional or keyword argument (see
5259
:func:`out_name`).
5360
5461
Args:
55-
fig (:class:`matplotlib.figure.Figure`): matplotlib figure.
56-
close (bool): whether to close the figure.
62+
fig: the :class:`matplotlib.figure.Figure` to save.
63+
close: whether to close the figure.
5764
name_args: positional arguments passed on to :func:`out_name`.
5865
name_kwargs: keyword arguments passed on to :func:`out_name`.
5966
"""
@@ -64,7 +71,7 @@ def saveplot(fig, *name_args, close=True, **name_kwargs):
6471
plt.close(fig)
6572

6673

67-
def baredoc(obj):
74+
def baredoc(obj: object) -> str:
6875
"""Return the first line of the docstring of an object.
6976
7077
Trailing periods and spaces as well as leading spaces are removed from the
@@ -82,12 +89,12 @@ def baredoc(obj):
8289
return doc.rstrip(' .').lstrip()
8390

8491

85-
def list_of_vars(arg_plot):
92+
def list_of_vars(arg_plot: str) -> List[List[List[str]]]:
8693
"""Construct list of variables per plot.
8794
8895
Args:
89-
arg_plot (str): string with variable names separated with
90-
``-`` (figures), ``.`` (subplots) and ``,`` (same subplot).
96+
arg_plot: variable names separated with ``-`` (figures),
97+
``.`` (subplots) and ``,`` (same subplot).
9198
Returns:
9299
three nested lists of str
93100
@@ -102,13 +109,13 @@ def list_of_vars(arg_plot):
102109
return [lov for lov in lovs if lov]
103110

104111

105-
def find_in_sorted_arr(value, array, after=False):
112+
def find_in_sorted_arr(value: Any, array: ndarray, after=False) -> int:
106113
"""Return position of element in a sorted array.
107114
108115
Returns:
109-
int: the maximum position i such as array[i] <= value. If after is
110-
True, it returns the min i such as value <= array[i] (or 0 if such
111-
an indices does not exist).
116+
the maximum position i such as array[i] <= value. If after is True, it
117+
returns the min i such as value <= array[i] (or 0 if such an index does
118+
not exist).
112119
"""
113120
ielt = array.searchsorted(value)
114121
if ielt == array.size:
@@ -118,7 +125,11 @@ def find_in_sorted_arr(value, array, after=False):
118125
return ielt
119126

120127

121-
class CachedReadOnlyProperty:
128+
T = TypeVar('T')
129+
V = TypeVar('V')
130+
131+
132+
class CachedReadOnlyProperty(Generic[T, V]):
122133
"""Descriptor implementation of read-only cached properties.
123134
124135
Properties are cached as ``_cropped_{name}`` instance attribute.
@@ -133,13 +144,13 @@ class CachedReadOnlyProperty:
133144
property is read-only instead of being writeable.
134145
"""
135146

136-
def __init__(self, thunk):
147+
def __init__(self, thunk: Callable[[T], V]):
137148
self._thunk = thunk
138149
self._name = thunk.__name__
139150
self._cache_name = f'_cropped_{self._name}'
140151
self.__doc__ = thunk.__doc__
141152

142-
def __get__(self, instance, _):
153+
def __get__(self, instance: T, _) -> V:
143154
try:
144155
return getattr(instance, self._cache_name)
145156
except AttributeError:
@@ -148,6 +159,6 @@ def __get__(self, instance, _):
148159
setattr(instance, self._cache_name, cached_value)
149160
return cached_value
150161

151-
def __set__(self, instance, _):
162+
def __set__(self, instance: T, _):
152163
raise AttributeError(
153164
f'Cannot set {self._name} property of {instance!r}')

0 commit comments

Comments
 (0)