diff --git a/lib/iris/common/metadata.py b/lib/iris/common/metadata.py index e11ea714626..f9349d9bba5 100644 --- a/lib/iris/common/metadata.py +++ b/lib/iris/common/metadata.py @@ -4,6 +4,8 @@ # 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 @@ -11,6 +13,7 @@ from functools import lru_cache, wraps import re +import cf_units import numpy as np import numpy.ma as ma from xxhash import xxh64_hexdigest @@ -151,6 +154,12 @@ class BaseMetadata(metaclass=_NamedTupleMeta): __slots__ = () + standard_name: str | None + long_name: str | None + var_name: str | None + units: cf_units.Unit + attributes: Mapping + @lenient_service def __eq__(self, other): """Determine whether the associated metadata members are equivalent. @@ -681,7 +690,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 diff --git a/lib/iris/common/mixin.py b/lib/iris/common/mixin.py index 2d9605de83a..8e89f0ccd06 100644 --- a/lib/iris/common/mixin.py +++ b/lib/iris/common/mixin.py @@ -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 @@ -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 @@ -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: @@ -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 diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 5c7f9da0eb0..d508133abc2 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -5,24 +5,27 @@ """Classes for representing multi-dimensional data with metadata.""" +from __future__ import annotations + from collections import OrderedDict -import copy -from copy import deepcopy -from functools import partial, reduce -import itertools -import operator -from typing import ( +from collections.abc import ( Container, Iterable, Iterator, Mapping, MutableMapping, - Optional, ) +import copy +from copy import deepcopy +from functools import partial, reduce +import itertools +import operator +from typing import TYPE_CHECKING, Optional import warnings from xml.dom.minidom import Document import zlib +import cf_units from cf_units import Unit import numpy as np import numpy.ma as ma @@ -36,11 +39,16 @@ from iris.analysis.cartography import wrap_lons import iris.analysis.maths import iris.aux_factory +from iris.aux_factory import AuxCoordFactory from iris.common import CFVariableMixin, CubeMetadata, metadata_manager_factory from iris.common.metadata import metadata_filter from iris.common.mixin import LimitedAttributeDict import iris.coord_systems import iris.coords +from iris.coords import AncillaryVariable, AuxCoord, CellMeasure, CellMethod, DimCoord + +if TYPE_CHECKING: + from iris.experimental.ugrid import MeshCoord import iris.exceptions import iris.util import iris.warnings @@ -1171,18 +1179,20 @@ def _walk_nodes(node): def __init__( self, - data, - standard_name=None, - long_name=None, - var_name=None, - units=None, - attributes=None, - cell_methods=None, - dim_coords_and_dims=None, - aux_coords_and_dims=None, - aux_factories=None, - cell_measures_and_dims=None, - ancillary_variables_and_dims=None, + data: np.typing.ArrayLike, + standard_name: str | None = None, + long_name: str | None = None, + var_name: str | None = None, + units: cf_units.Unit | str | None = None, + attributes: Mapping | None = None, + cell_methods: Iterable[CellMethod] | None = None, + dim_coords_and_dims: Iterable[tuple[DimCoord, int]] | None = None, + aux_coords_and_dims: Iterable[tuple[AuxCoord, int | Iterable[int]]] + | None = None, + aux_factories: Iterable[AuxCoordFactory] | None = None, + cell_measures_and_dims: Iterable[tuple[CellMeasure, int]] | None = None, + ancillary_variables_and_dims: Iterable[tuple[AncillaryVariable, int]] + | None = None, ): """Create a cube with data and optional metadata. @@ -1269,22 +1279,28 @@ def __init__( #: The NetCDF variable name for the Cube. self.var_name = var_name - self.cell_methods = cell_methods + # See https://github.com/python/mypy/issues/3004. + self.cell_methods = cell_methods # type: ignore[assignment] #: A dictionary for arbitrary Cube metadata. #: A few keys are restricted - see :class:`CubeAttrsDict`. - self.attributes = attributes + # See https://github.com/python/mypy/issues/3004. + self.attributes = attributes # type: ignore[assignment] # Coords - self._dim_coords_and_dims = [] - self._aux_coords_and_dims = [] - self._aux_factories = [] + self._dim_coords_and_dims: list[tuple[DimCoord, int]] = [] + self._aux_coords_and_dims: list[ + tuple[AuxCoord | DimCoord, tuple[int, ...]] + ] = [] + self._aux_factories: list[AuxCoordFactory] = [] # Cell Measures - self._cell_measures_and_dims = [] + self._cell_measures_and_dims: list[tuple[CellMeasure, tuple[int, ...]]] = [] # Ancillary Variables - self._ancillary_variables_and_dims = [] + self._ancillary_variables_and_dims: list[ + tuple[AncillaryVariable, tuple[int, ...]] + ] = [] identities = set() if dim_coords_and_dims: @@ -1299,12 +1315,12 @@ def __init__( dims.add(dim) if aux_coords_and_dims: - for coord, dims in aux_coords_and_dims: - identity = coord.standard_name, coord.long_name + for auxcoord, auxdims in aux_coords_and_dims: + identity = auxcoord.standard_name, auxcoord.long_name if identity not in identities: - self._add_unique_aux_coord(coord, dims) + self._add_unique_aux_coord(auxcoord, auxdims) else: - self.add_aux_coord(coord, dims) + self.add_aux_coord(auxcoord, auxdims) identities.add(identity) if aux_factories: @@ -1312,12 +1328,12 @@ def __init__( self.add_aux_factory(factory) if cell_measures_and_dims: - for cell_measure, dims in cell_measures_and_dims: - self.add_cell_measure(cell_measure, dims) + for cell_measure, cmdims in cell_measures_and_dims: + self.add_cell_measure(cell_measure, cmdims) if ancillary_variables_and_dims: - for ancillary_variable, dims in ancillary_variables_and_dims: - self.add_ancillary_variable(ancillary_variable, dims) + for ancillary_variable, avdims in ancillary_variables_and_dims: + self.add_ancillary_variable(ancillary_variable, avdims) @property def _names(self): @@ -1334,12 +1350,12 @@ def _names(self): # # Ensure that .attributes is always a :class:`CubeAttrsDict`. # - @property + @property # type: ignore[override] def attributes(self) -> CubeAttrsDict: - return super().attributes + return super().attributes # type: ignore[return-value] @attributes.setter - def attributes(self, attributes: Optional[Mapping]): + def attributes(self, attributes: Optional[Mapping]) -> None: """Override to CfVariableMixin.attributes.setter. An override to CfVariableMixin.attributes.setter, which ensures that Cube @@ -1462,11 +1478,15 @@ def convert_units(self, unit): self.data = new_data self.units = unit - def add_cell_method(self, cell_method): + def add_cell_method(self, cell_method: CellMethod) -> None: """Add a :class:`~iris.coords.CellMethod` to the Cube.""" self.cell_methods += (cell_method,) - def add_aux_coord(self, coord, data_dims=None): + def add_aux_coord( + self, + coord: DimCoord | AuxCoord, + data_dims: Iterable[int] | int | None = None, + ) -> None: """Add a CF auxiliary coordinate to the cube. Parameters @@ -1496,11 +1516,15 @@ def add_aux_coord(self, coord, data_dims=None): ) self._add_unique_aux_coord(coord, data_dims) - def _check_multi_dim_metadata(self, metadata, data_dims): + def _check_multi_dim_metadata( + self, + metadata, + data_dims: Iterable[int] | int | None, + ) -> tuple[int, ...]: # Convert to a tuple of integers if data_dims is None: data_dims = tuple() - elif isinstance(data_dims, Container): + elif isinstance(data_dims, Iterable): data_dims = tuple(int(d) for d in data_dims) else: data_dims = (int(data_dims),) @@ -1576,7 +1600,7 @@ def _add_unique_aux_coord(self, coord, data_dims): self._aux_coords_and_dims.append((coord, data_dims)) - def add_aux_factory(self, aux_factory): + def add_aux_factory(self, aux_factory: AuxCoordFactory) -> None: """Add an auxiliary coordinate factory to the cube. Parameters @@ -1608,7 +1632,11 @@ def coordsonly(coords_and_dims): ) self._aux_factories.append(aux_factory) - def add_cell_measure(self, cell_measure, data_dims=None): + def add_cell_measure( + self, + cell_measure: CellMeasure, + data_dims: Iterable[int] | int | None = None, + ) -> None: """Add a CF cell measure to the cube. Parameters @@ -1672,7 +1700,7 @@ def add_ancillary_variable(self, ancillary_variable, data_dims=None): key=lambda av_dims: (av_dims[0].metadata, av_dims[1]) ) - def add_dim_coord(self, dim_coord, data_dim): + def add_dim_coord(self, dim_coord: DimCoord, data_dim: int) -> None: """Add a CF coordinate to the cube. Parameters @@ -1707,7 +1735,7 @@ def add_dim_coord(self, dim_coord, data_dim): ) self._add_unique_dim_coord(dim_coord, data_dim) - def _add_unique_dim_coord(self, dim_coord, data_dim): + def _add_unique_dim_coord(self, dim_coord: DimCoord, data_dim: int | tuple[int]): if isinstance(dim_coord, iris.coords.AuxCoord): raise iris.exceptions.CannotAddError( "The dim_coord may not be an AuxCoord instance." @@ -1743,7 +1771,7 @@ def _add_unique_dim_coord(self, dim_coord, data_dim): self._dim_coords_and_dims.append((dim_coord, int(data_dim))) - def remove_aux_factory(self, aux_factory): + def remove_aux_factory(self, aux_factory: AuxCoordFactory) -> None: """Remove the given auxiliary coordinate factory from the cube.""" self._aux_factories.remove(aux_factory) @@ -1762,7 +1790,7 @@ def _remove_coord(self, coord): if coord.metadata == aux_factory.metadata: self.remove_aux_factory(aux_factory) - def remove_coord(self, coord): + def remove_coord(self, coord: str | DimCoord | AuxCoord) -> None: """Remove a coordinate from the cube. Parameters @@ -2027,18 +2055,18 @@ def aux_factory(self, name=None, standard_name=None, long_name=None, var_name=No def coords( self, - name_or_coord=None, - standard_name=None, - long_name=None, - var_name=None, - attributes=None, - axis=None, + name_or_coord: str | DimCoord | AuxCoord | MeshCoord | None = None, + standard_name: str | None = None, + long_name: str | None = None, + var_name: str | None = None, + attributes: Mapping | None = None, + axis: iris.util.Axis | None = None, contains_dimension=None, - dimensions=None, + dimensions: Iterable[int] | int | None = None, coord_system=None, - dim_coords=None, - mesh_coords=None, - ): + dim_coords: bool | None = None, + mesh_coords: bool | None = None, + ) -> list[DimCoord | AuxCoord | MeshCoord]: r"""Return a list of coordinates from the :class:`Cube` that match the provided criteria. Parameters @@ -2153,7 +2181,7 @@ def coords( ] if dimensions is not None: - if not isinstance(dimensions, Container): + if not isinstance(dimensions, Iterable): dimensions = [dimensions] dimensions = tuple(dimensions) coords_and_factories = [ @@ -2184,18 +2212,18 @@ def extract_coord(coord_or_factory): def coord( self, - name_or_coord=None, - standard_name=None, - long_name=None, - var_name=None, - attributes=None, - axis=None, + name_or_coord: str | DimCoord | AuxCoord | MeshCoord | None = None, + standard_name: str | None = None, + long_name: str | None = None, + var_name: str | None = None, + attributes: Mapping | None = None, + axis: iris.util.Axis | None = None, contains_dimension=None, - dimensions=None, + dimensions: Iterable[int] | int | None = None, coord_system=None, - dim_coords=None, - mesh_coords=None, - ): + dim_coords: bool | None = None, + mesh_coords: bool | None = None, + ) -> DimCoord | AuxCoord | MeshCoord: r"""Return a single coordinate from the :class:`Cube` that matches the provided criteria. Parameters @@ -2606,7 +2634,7 @@ def ancillary_variable(self, name_or_ancillary_variable=None): return ancillary_variables[0] @property - def cell_methods(self): + def cell_methods(self) -> tuple[CellMethod, ...]: """Tuple of :class:`iris.coords.CellMethod`. Tuple of :class:`iris.coords.CellMethod` representing the processing @@ -2616,7 +2644,10 @@ def cell_methods(self): return self._metadata_manager.cell_methods @cell_methods.setter - def cell_methods(self, cell_methods: Iterable): + def cell_methods( + self, + cell_methods: Iterable[CellMethod] | None, + ) -> None: if not cell_methods: # For backwards compatibility: Empty or null value is equivalent to (). cell_methods = () @@ -2627,7 +2658,7 @@ def cell_methods(self, cell_methods: Iterable): # All contents should be CellMethods. Requiring class membership is # somewhat non-Pythonic, but simple, and not a problem for now. if not isinstance(cell_method, iris.coords.CellMethod): - msg = ( + msg = ( # type: ignore[unreachable] f"Cube.cell_methods assigned value includes {cell_method}, " "which is not an iris.coords.CellMethod." ) diff --git a/lib/iris/experimental/ugrid/utils.py b/lib/iris/experimental/ugrid/utils.py index 0074619bf22..be1365d9fcb 100644 --- a/lib/iris/experimental/ugrid/utils.py +++ b/lib/iris/experimental/ugrid/utils.py @@ -134,23 +134,23 @@ def recombine_submeshes( for i_dim in range(mesh_cube.ndim): if i_dim == mesh_dim: # mesh dim : look for index coords (by name) - full_coord = mesh_cube.coords( + full_coords = mesh_cube.coords( name_or_coord=index_coord_name, dimensions=(i_dim,) ) - sub_coord = cube.coords( + sub_coords = cube.coords( name_or_coord=index_coord_name, dimensions=(i_dim,) ) else: # non-mesh dims : look for dim-coords (only) - full_coord = mesh_cube.coords(dim_coords=True, dimensions=(i_dim,)) - sub_coord = cube.coords(dim_coords=True, dimensions=(i_dim,)) + full_coords = mesh_cube.coords(dim_coords=True, dimensions=(i_dim,)) + sub_coords = cube.coords(dim_coords=True, dimensions=(i_dim,)) - if full_coord: - (full_coord,) = full_coord + if full_coords: + (full_coord,) = full_coords full_dimname = full_coord.name() full_metadata = full_coord.metadata._replace(var_name=None) - if sub_coord: - (sub_coord,) = sub_coord + if sub_coords: + (sub_coord,) = sub_coords sub_dimname = sub_coord.name() sub_metadata = sub_coord.metadata._replace(var_name=None) @@ -158,18 +158,18 @@ def recombine_submeshes( # N.B. checks for mesh- and non-mesh-dims are different if i_dim != mesh_dim: # i_dim == mesh_dim : checks for non-mesh dims - if full_coord and not sub_coord: + if full_coords and not sub_coords: err = ( f"{sub_str} has no dim-coord for dimension " f"{i_dim}, to match the 'mesh_cube' dimension " f'"{full_dimname}".' ) - elif sub_coord and not full_coord: + elif sub_coords and not full_coords: err = ( f'{sub_str} has a dim-coord "{sub_dimname}" for ' f"dimension {i_dim}, but 'mesh_cube' has none." ) - elif sub_coord != full_coord: + elif sub_coords != full_coords: err = ( f'{sub_str} has a dim-coord "{sub_dimname}" for ' f"dimension {i_dim}, which does not match that " @@ -177,13 +177,13 @@ def recombine_submeshes( ) else: # i_dim == mesh_dim : different rules for this one - if not sub_coord: + if not sub_coords: # Must have an index coord on the mesh dimension err = ( f'{sub_str} has no "{index_coord_name}" coord on ' f"the mesh dimension (dimension {mesh_dim})." ) - elif full_coord and sub_metadata != full_metadata: + elif full_coords and sub_metadata != full_metadata: # May *not* have full-cube index, but if so it must match err = ( f"{sub_str} has an index coord " diff --git a/lib/iris/util.py b/lib/iris/util.py index 4924ca68d2b..5781ed8f620 100644 --- a/lib/iris/util.py +++ b/lib/iris/util.py @@ -14,6 +14,7 @@ import os.path import sys import tempfile +from typing import Literal import cf_units from dask import array as da @@ -252,7 +253,10 @@ def describe_diff(cube_a, cube_b, output_file=None): ) -def guess_coord_axis(coord): +Axis = Literal["X", "Y", "Z", "T"] + + +def guess_coord_axis(coord) -> Axis | None: """Return a "best guess" axis name of the coordinate. Heuristic categorisation of the coordinate into either label @@ -276,7 +280,7 @@ def guess_coord_axis(coord): :attr:`~iris.coords.Coord.ignore_axis` property on `coord` to ``False``. """ - axis = None + axis: Axis | None = None if hasattr(coord, "ignore_axis") and coord.ignore_axis is True: return axis