Skip to content

Commit

Permalink
Add type hints to iris.cube.Cube (#6037)
Browse files Browse the repository at this point in the history
* Add some type hints

* Add more type hints

* Enable type hints in docstrings

* Avoid warning

* Python 3.10 compatibility

* Remove duplicate type definitions from docstrings

* Undo accidental change

* Ignore coverage for TYPE_CHECKING imports

* Add whatsnew entry

* Remove N/A line

* Fix type hints
  • Loading branch information
bouweandela authored Sep 16, 2024
1 parent 2acfd92 commit 9ee671c
Show file tree
Hide file tree
Showing 16 changed files with 618 additions and 372 deletions.
6 changes: 4 additions & 2 deletions benchmarks/benchmarks/cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
# See LICENSE in the root of the repository for full licensing details.
"""Cube benchmark tests."""

from collections.abc import Iterable

from iris import coords
from iris.cube import Cube

Expand All @@ -21,9 +23,9 @@ def setup(self, w_mesh: bool, _) -> None:
source_cube = realistic_4d_w_everything(w_mesh=w_mesh)

def get_coords_and_dims(
coords_tuple: tuple[coords._DimensionalMetadata, ...],
coords_iter: Iterable[coords._DimensionalMetadata],
) -> list[tuple[coords._DimensionalMetadata, tuple[int, ...]]]:
return [(c, c.cube_dims(source_cube)) for c in coords_tuple]
return [(c, c.cube_dims(source_cube)) for c in coords_iter]

self.cube_kwargs = dict(
data=source_cube.data,
Expand Down
10 changes: 5 additions & 5 deletions docs/src/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ def _dotv(version):
}

# https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autodoc_typehints
autodoc_typehints = "none"
autodoc_typehints = "description"
autosummary_generate = True
autosummary_imported_members = True
autopackage_name = ["iris"]
Expand Down Expand Up @@ -246,17 +246,17 @@ def _dotv(version):
# See https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html
intersphinx_mapping = {
"cartopy": ("https://scitools.org.uk/cartopy/docs/latest/", None),
"cf_units": ("https://cf-units.readthedocs.io/en/stable/", None),
"cftime": ("https://unidata.github.io/cftime/", None),
"dask": ("https://docs.dask.org/en/stable/", None),
"geovista": ("https://geovista.readthedocs.io/en/latest/", None),
"iris-esmf-regrid": ("https://iris-esmf-regrid.readthedocs.io/en/stable/", None),
"matplotlib": ("https://matplotlib.org/stable/", None),
"numpy": ("https://numpy.org/doc/stable/", None),
"python": ("https://docs.python.org/3/", None),
"scipy": ("https://docs.scipy.org/doc/scipy/", None),
"pandas": ("https://pandas.pydata.org/docs/", None),
"dask": ("https://docs.dask.org/en/stable/", None),
"geovista": ("https://geovista.readthedocs.io/en/latest/", None),
"python": ("https://docs.python.org/3/", None),
"pyvista": ("https://docs.pyvista.org/", None),
"scipy": ("https://docs.scipy.org/doc/scipy/", None),
}

# The name of the Pygments (syntax highlighting) style to use.
Expand Down
3 changes: 1 addition & 2 deletions docs/src/whatsnew/latest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,7 @@ This document explains the changes made to Iris for this release
📚 Documentation
================

#. N/A

#. `@bouweandela`_ added type hints for :class:`~iris.cube.Cube`. (:pull:`6037`)

💼 Internal
===========
Expand Down
16 changes: 8 additions & 8 deletions lib/iris/_concatenate.py
Original file line number Diff line number Diff line change
Expand Up @@ -690,9 +690,9 @@ def __init__(self, cube: iris.cube.Cube) -> None:
#
# Collate the dimension coordinate metadata.
#
for coord in self.dim_coords:
dims = cube.coord_dims(coord)
self.dim_metadata.append(_CoordMetaData(coord, dims))
for dim_coord in self.dim_coords:
dims = cube.coord_dims(dim_coord)
self.dim_metadata.append(_CoordMetaData(dim_coord, dims))
self.dim_mapping.append(dims[0])

#
Expand All @@ -709,13 +709,13 @@ def key_func(coord):
cube.coord_dims(coord),
)

for coord in sorted(cube.aux_coords, key=key_func):
dims = cube.coord_dims(coord)
for aux_coord in sorted(cube.aux_coords, key=key_func):
dims = cube.coord_dims(aux_coord)
if dims:
self.aux_metadata.append(_CoordMetaData(coord, dims))
self.aux_coords_and_dims.append(_CoordAndDims(coord, tuple(dims)))
self.aux_metadata.append(_CoordMetaData(aux_coord, dims))
self.aux_coords_and_dims.append(_CoordAndDims(aux_coord, tuple(dims)))
else:
self.scalar_coords.append(coord)
self.scalar_coords.append(aux_coord)

def meta_key_func(dm):
return (dm.metadata, dm.cube_dims(cube))
Expand Down
2 changes: 1 addition & 1 deletion lib/iris/_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ def list_of_constraints(constraints):
return [as_constraint(constraint) for constraint in constraints]


def as_constraint(thing):
def as_constraint(thing: Constraint | str | None) -> Constraint:
"""Cast an object into a cube constraint where possible.
Cast an object into a cube constraint where possible, otherwise
Expand Down
53 changes: 42 additions & 11 deletions lib/iris/analysis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@

from __future__ import annotations

from collections.abc import Iterable
from collections.abc import Iterable, Sequence
import functools
from functools import wraps
from inspect import getfullargspec
import itertools
from numbers import Number
from typing import Optional, Union
from typing import Optional, Protocol, Union
import warnings

from cf_units import Unit
Expand All @@ -56,7 +56,7 @@
from iris.analysis._interpolation import EXTRAPOLATION_MODES, RectilinearInterpolator
from iris.analysis._regrid import CurvilinearRegridder, RectilinearRegridder
import iris.coords
from iris.coords import _DimensionalMetadata
from iris.coords import AuxCoord, DimCoord, _DimensionalMetadata
from iris.exceptions import LazyAggregatorError
import iris.util

Expand Down Expand Up @@ -2288,8 +2288,8 @@ class _Groupby:

def __init__(
self,
groupby_coords: list[iris.coords.Coord],
shared_coords: Optional[list[tuple[iris.coords.Coord, int]]] = None,
groupby_coords: Iterable[AuxCoord | DimCoord],
shared_coords: Optional[Iterable[tuple[AuxCoord | DimCoord, int]]] = None,
climatological: bool = False,
) -> None:
"""Determine the group slices over the group-by coordinates.
Expand All @@ -2310,9 +2310,9 @@ def __init__(
"""
#: Group-by and shared coordinates that have been grouped.
self.coords: list[iris.coords.Coord] = []
self._groupby_coords: list[iris.coords.Coord] = []
self._shared_coords: list[tuple[iris.coords.Coord, int]] = []
self.coords: list[AuxCoord | DimCoord] = []
self._groupby_coords: list[AuxCoord | DimCoord] = []
self._shared_coords: list[tuple[AuxCoord | DimCoord, int]] = []
self._groupby_indices: list[tuple[int, ...]] = []
self._stop = None
# Ensure group-by coordinates are iterable.
Expand All @@ -2338,10 +2338,10 @@ def __init__(
# Stores mapping from original cube coords to new ones, as metadata may
# not match
self.coord_replacement_mapping: list[
tuple[iris.coords.Coord, iris.coords.Coord]
tuple[AuxCoord | DimCoord, AuxCoord | DimCoord]
] = []

def _add_groupby_coord(self, coord: iris.coords.Coord) -> None:
def _add_groupby_coord(self, coord: AuxCoord | DimCoord) -> None:
if coord.ndim != 1:
raise iris.exceptions.CoordinateMultiDimError(coord)
if self._stop is None:
Expand All @@ -2350,7 +2350,7 @@ def _add_groupby_coord(self, coord: iris.coords.Coord) -> None:
raise ValueError("Group-by coordinates have different lengths.")
self._groupby_coords.append(coord)

def _add_shared_coord(self, coord: iris.coords.Coord, dim: int) -> None:
def _add_shared_coord(self, coord: AuxCoord | DimCoord, dim: int) -> None:
if coord.shape[dim] != self._stop and self._stop is not None:
raise ValueError("Shared coordinates have different lengths.")
self._shared_coords.append((coord, dim))
Expand Down Expand Up @@ -2583,6 +2583,37 @@ def clear_phenomenon_identity(cube):
###############################################################################


class Interpolator(Protocol):
def __call__( # noqa: E704 # ruff formatting conflicts with flake8
self,
sample_points: Sequence[np.typing.ArrayLike],
collapse_scalar: bool,
) -> iris.cube.Cube: ...


class InterpolationScheme(Protocol):
def interpolator( # noqa: E704 # ruff formatting conflicts with flake8
self,
cube: iris.cube.Cube,
coords: AuxCoord | DimCoord | str,
) -> Interpolator: ...


class Regridder(Protocol):
def __call__( # noqa: E704 # ruff formatting conflicts with flake8
self,
src: iris.cube.Cube,
) -> iris.cube.Cube: ...


class RegriddingScheme(Protocol):
def regridder( # noqa: E704 # ruff formatting conflicts with flake8
self,
src_grid: iris.cube.Cube,
target_grid: iris.cube.Cube,
) -> Regridder: ...


class Linear:
"""Describes the linear interpolation and regridding scheme.
Expand Down
2 changes: 1 addition & 1 deletion lib/iris/analysis/cartography.py
Original file line number Diff line number Diff line change
Expand Up @@ -1128,7 +1128,7 @@ def rotate_winds(u_cube, v_cube, target_cs):
Returns
-------
(u', v') tuple of :class:`iris.cube.Cube`
tuple of :class:`iris.cube.Cube`
A (u', v') tuple of :class:`iris.cube.Cube` instances that are the u
and v components in the requested target coordinate system.
The units are the same as the inputs.
Expand Down
20 changes: 17 additions & 3 deletions lib/iris/common/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,23 @@
# See LICENSE in the root of the repository for full licensing details.
"""Provides the infrastructure to support the common metadata API."""

from __future__ import annotations

from abc import ABCMeta
from collections import namedtuple
from collections.abc import Iterable, Mapping
from copy import deepcopy
from functools import lru_cache, wraps
import re
from typing import TYPE_CHECKING, Any

import cf_units
import numpy as np
import numpy.ma as ma
from xxhash import xxh64_hexdigest

if TYPE_CHECKING:
from iris.coords import CellMethod
from ..config import get_logger
from ._split_attribute_dicts import adjust_for_split_attribute_dictionaries
from .lenient import _LENIENT
Expand Down Expand Up @@ -153,6 +159,12 @@ class BaseMetadata(metaclass=_NamedTupleMeta):

__slots__ = ()

standard_name: str | None
long_name: str | None
var_name: str | None
units: cf_units.Unit
attributes: Any

@lenient_service
def __eq__(self, other):
"""Determine whether the associated metadata members are equivalent.
Expand Down Expand Up @@ -683,7 +695,7 @@ def from_metadata(cls, other):
result = cls(**kwargs)
return result

def name(self, default=None, token=False):
def name(self, default: str | None = None, token: bool = False) -> str:
"""Return a string name representing the identity of the metadata.
First it tries standard name, then it tries the long name, then
Expand All @@ -692,10 +704,10 @@ def name(self, default=None, token=False):
Parameters
----------
default : optional
default :
The fall-back string representing the default name. Defaults to
the string 'unknown'.
token : bool, default=False
token :
If True, ensures that the name returned satisfies the criteria for
the characters required by a valid NetCDF name. If it is not
possible to return a valid name, then a ValueError exception is
Expand Down Expand Up @@ -1039,6 +1051,8 @@ class CubeMetadata(BaseMetadata):

_members = "cell_methods"

cell_methods: tuple[CellMethod, ...]

__slots__ = ()

@wraps(BaseMetadata.__eq__, assigned=("__doc__",), updated=())
Expand Down
33 changes: 21 additions & 12 deletions lib/iris/common/mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
# See LICENSE in the root of the repository for full licensing details.
"""Provides common metadata mixin behaviour."""

from __future__ import annotations

from collections.abc import Mapping
from functools import wraps
from typing import Any

import cf_units

Expand Down Expand Up @@ -138,11 +141,17 @@ def update(self, other, **kwargs):


class CFVariableMixin:
_metadata_manager: Any

@wraps(BaseMetadata.name)
def name(self, default=None, token=None):
def name(
self,
default: str | None = None,
token: bool | None = None,
) -> str:
return self._metadata_manager.name(default=default, token=token)

def rename(self, name):
def rename(self, name: str | None) -> None:
"""Change the human-readable name.
If 'name' is a valid standard name it will assign it to
Expand All @@ -161,30 +170,30 @@ def rename(self, name):
self.var_name = None

@property
def standard_name(self):
def standard_name(self) -> str | None:
"""The CF Metadata standard name for the object."""
return self._metadata_manager.standard_name

@standard_name.setter
def standard_name(self, name):
def standard_name(self, name: str | None) -> None:
self._metadata_manager.standard_name = _get_valid_standard_name(name)

@property
def long_name(self):
def long_name(self) -> str | None:
"""The CF Metadata long name for the object."""
return self._metadata_manager.long_name

@long_name.setter
def long_name(self, name):
def long_name(self, name: str | None) -> None:
self._metadata_manager.long_name = name

@property
def var_name(self):
def var_name(self) -> str | None:
"""The NetCDF variable name for the object."""
return self._metadata_manager.var_name

@var_name.setter
def var_name(self, name):
def var_name(self, name: str | None) -> None:
if name is not None:
result = self._metadata_manager.token(name)
if result is None or not name:
Expand All @@ -193,20 +202,20 @@ def var_name(self, name):
self._metadata_manager.var_name = name

@property
def units(self):
def units(self) -> cf_units.Unit:
"""The S.I. unit of the object."""
return self._metadata_manager.units

@units.setter
def units(self, unit):
def units(self, unit: cf_units.Unit | str | None) -> None:
self._metadata_manager.units = cf_units.as_unit(unit)

@property
def attributes(self):
def attributes(self) -> LimitedAttributeDict:
return self._metadata_manager.attributes

@attributes.setter
def attributes(self, attributes):
def attributes(self, attributes: Mapping) -> None:
self._metadata_manager.attributes = LimitedAttributeDict(attributes or {})

@property
Expand Down
Loading

0 comments on commit 9ee671c

Please sign in to comment.