diff --git a/lib/iris/_concatenate.py b/lib/iris/_concatenate.py index 2a73fcacea..0053a555f7 100644 --- a/lib/iris/_concatenate.py +++ b/lib/iris/_concatenate.py @@ -5,7 +5,6 @@ """Automatic concatenation of multiple cubes over one or more existing dimensions.""" from collections import defaultdict, namedtuple -import warnings import dask.array as da import numpy as np @@ -13,6 +12,7 @@ import iris.coords import iris.cube import iris.exceptions +from iris.exceptions import warn_once_at_level from iris.util import array_equal, guess_coord_axis # @@ -911,7 +911,7 @@ def register( raise iris.exceptions.ConcatenateError([msg]) elif not match: msg = f"Found cubes with overlap on concatenate axis {candidate_axis}, skipping concatenation for these cubes" - warnings.warn(msg, category=iris.exceptions.IrisUserWarning) + warn_once_at_level(msg, category=iris.exceptions.IrisUserWarning) # Check for compatible AuxCoords. if match: diff --git a/lib/iris/_deprecation.py b/lib/iris/_deprecation.py index b771883a71..0870061d8b 100644 --- a/lib/iris/_deprecation.py +++ b/lib/iris/_deprecation.py @@ -4,7 +4,7 @@ # See LICENSE in the root of the repository for full licensing details. """Utilities for producing runtime deprecation messages.""" -import warnings +from iris.exceptions import warn_once_at_level class IrisDeprecation(UserWarning): @@ -44,7 +44,7 @@ def warn_deprecated(msg, stacklevel=2): >>> """ - warnings.warn(msg, category=IrisDeprecation, stacklevel=stacklevel) + warn_once_at_level(msg, category=IrisDeprecation, stacklevel=stacklevel) # A Mixin for a wrapper class that copies the docstring of the wrapped class diff --git a/lib/iris/analysis/_regrid.py b/lib/iris/analysis/_regrid.py index 4a8ba0bb67..982a2de0ff 100644 --- a/lib/iris/analysis/_regrid.py +++ b/lib/iris/analysis/_regrid.py @@ -5,7 +5,6 @@ import copy import functools -import warnings import numpy as np import numpy.ma as ma @@ -19,7 +18,7 @@ snapshot_grid, ) from iris.analysis._scipy_interpolate import _RegularGridInterpolator -from iris.exceptions import IrisImpossibleUpdateWarning +from iris.exceptions import IrisImpossibleUpdateWarning, warn_once_at_level from iris.util import _meshgrid, guess_coord_axis @@ -1106,6 +1105,6 @@ def regrid_reference_surface( "Cannot update aux_factory {!r} because of dropped" " coordinates.".format(factory.name()) ) - warnings.warn(msg, category=IrisImpossibleUpdateWarning) + warn_once_at_level(msg, category=IrisImpossibleUpdateWarning) return result diff --git a/lib/iris/analysis/calculus.py b/lib/iris/analysis/calculus.py index 560b9b1d5c..992b776d56 100644 --- a/lib/iris/analysis/calculus.py +++ b/lib/iris/analysis/calculus.py @@ -9,7 +9,6 @@ """ import re -import warnings import cf_units import numpy as np @@ -22,7 +21,7 @@ import iris.analysis.maths import iris.coord_systems import iris.coords -from iris.exceptions import IrisUserWarning +from iris.exceptions import IrisUserWarning, warn_once_at_level from iris.util import delta __all__ = ["DIRECTIONAL_NAMES", "cube_delta", "curl", "differentiate"] @@ -85,7 +84,7 @@ def _construct_midpoint_coord(coord, circular=None): "Construction coordinate midpoints for the '{}' coordinate, " "though it has the attribute 'circular'={}." ) - warnings.warn( + warn_once_at_level( msg.format(circular, coord.circular, coord.name()), category=IrisUserWarning, ) diff --git a/lib/iris/analysis/cartography.py b/lib/iris/analysis/cartography.py index d57f8ce8a3..b69d1e57d1 100644 --- a/lib/iris/analysis/cartography.py +++ b/lib/iris/analysis/cartography.py @@ -6,7 +6,6 @@ from collections import namedtuple import copy -import warnings import cartopy.crs as ccrs import cartopy.img_transform @@ -18,6 +17,7 @@ import iris.coord_systems import iris.coords import iris.exceptions +from iris.exceptions import warn_once_at_level from iris.util import _meshgrid from ._grid_angles import gridcell_angles, rotate_grid_vectors @@ -410,7 +410,7 @@ def area_weights(cube, normalize=False): cs = cube.coord_system("CoordSystem") if isinstance(cs, iris.coord_systems.GeogCS): if cs.inverse_flattening != 0.0: - warnings.warn( + warn_once_at_level( "Assuming spherical earth from ellipsoid.", category=iris.exceptions.IrisDefaultingWarning, ) @@ -419,13 +419,13 @@ def area_weights(cube, normalize=False): cs.ellipsoid is not None ): if cs.ellipsoid.inverse_flattening != 0.0: - warnings.warn( + warn_once_at_level( "Assuming spherical earth from ellipsoid.", category=iris.exceptions.IrisDefaultingWarning, ) radius_of_earth = cs.ellipsoid.semi_major_axis else: - warnings.warn( + warn_once_at_level( "Using DEFAULT_SPHERICAL_EARTH_RADIUS.", category=iris.exceptions.IrisDefaultingWarning, ) @@ -567,7 +567,7 @@ def cosine_latitude_weights(cube): if np.any(lat.points < -np.pi / 2.0 - threshold) or np.any( lat.points > np.pi / 2.0 + threshold ): - warnings.warn( + warn_once_at_level( "Out of range latitude values will be clipped to the valid range.", category=iris.exceptions.IrisDefaultingWarning, ) @@ -683,7 +683,7 @@ def project(cube, target_proj, nx=None, ny=None): # Determine source coordinate system if lat_coord.coord_system is None: # Assume WGS84 latlon if unspecified - warnings.warn( + warn_once_at_level( "Coordinate system of latitude and longitude " "coordinates is not specified. Assuming WGS84 Geodetic.", category=iris.exceptions.IrisDefaultingWarning, @@ -871,7 +871,7 @@ def project(cube, target_proj, nx=None, ny=None): new_cube.add_aux_coord(coord.copy(), cube.coord_dims(coord)) discarded_coords = coords_to_ignore.difference([lat_coord, lon_coord]) if discarded_coords: - warnings.warn( + warn_once_at_level( "Discarding coordinates that share dimensions with {} and {}: {}".format( lat_coord.name(), lon_coord.name(), diff --git a/lib/iris/analysis/geometry.py b/lib/iris/analysis/geometry.py index 271858716c..2be351ea50 100644 --- a/lib/iris/analysis/geometry.py +++ b/lib/iris/analysis/geometry.py @@ -9,12 +9,11 @@ """ -import warnings - import numpy as np from shapely.geometry import Polygon import iris.exceptions +from iris.exceptions import warn_once_at_level def _extract_relevant_cube_slice(cube, geometry): @@ -69,7 +68,7 @@ def _extract_relevant_cube_slice(cube, geometry): x_min_ix = np.where(x_bounds_lower <= x_min_geom)[0] x_min_ix = x_min_ix[np.argmax(x_bounds_lower[x_min_ix])] except ValueError: - warnings.warn( + warn_once_at_level( "The geometry exceeds the cube's x dimension at the lower end.", category=iris.exceptions.IrisGeometryExceedWarning, ) @@ -79,7 +78,7 @@ def _extract_relevant_cube_slice(cube, geometry): x_max_ix = np.where(x_bounds_upper >= x_max_geom)[0] x_max_ix = x_max_ix[np.argmin(x_bounds_upper[x_max_ix])] except ValueError: - warnings.warn( + warn_once_at_level( "The geometry exceeds the cube's x dimension at the upper end.", category=iris.exceptions.IrisGeometryExceedWarning, ) @@ -89,7 +88,7 @@ def _extract_relevant_cube_slice(cube, geometry): y_min_ix = np.where(y_bounds_lower <= y_min_geom)[0] y_min_ix = y_min_ix[np.argmax(y_bounds_lower[y_min_ix])] except ValueError: - warnings.warn( + warn_once_at_level( "The geometry exceeds the cube's y dimension at the lower end.", category=iris.exceptions.IrisGeometryExceedWarning, ) @@ -99,7 +98,7 @@ def _extract_relevant_cube_slice(cube, geometry): y_max_ix = np.where(y_bounds_upper >= y_max_geom)[0] y_max_ix = y_max_ix[np.argmin(y_bounds_upper[y_max_ix])] except ValueError: - warnings.warn( + warn_once_at_level( "The geometry exceeds the cube's y dimension at the upper end.", category=iris.exceptions.IrisGeometryExceedWarning, ) diff --git a/lib/iris/analysis/maths.py b/lib/iris/analysis/maths.py index f20972d1e5..2cb6ac8980 100644 --- a/lib/iris/analysis/maths.py +++ b/lib/iris/analysis/maths.py @@ -8,7 +8,6 @@ import inspect import math import operator -import warnings import cf_units import dask.array as da @@ -22,6 +21,7 @@ from iris.config import get_logger import iris.coords import iris.exceptions +from iris.exceptions import warn_once_at_level import iris.util # Configure the logger. @@ -938,7 +938,7 @@ def _broadcast_cube_coord_data(cube, other, operation_name, dim=None): raise iris.exceptions.CoordinateMultiDimError(other) if other.has_bounds(): - warnings.warn( + warn_once_at_level( "Using {!r} with a bounded coordinate is not well " "defined; ignoring bounds.".format(operation_name), category=iris.exceptions.IrisIgnoringBoundsWarning, diff --git a/lib/iris/aux_factory.py b/lib/iris/aux_factory.py index 61b7c7ef46..e0a5e51a1d 100644 --- a/lib/iris/aux_factory.py +++ b/lib/iris/aux_factory.py @@ -5,7 +5,6 @@ """Definitions of derived coordinates.""" from abc import ABCMeta, abstractmethod -import warnings import cf_units import dask.array as da @@ -13,7 +12,7 @@ from iris.common import CFVariableMixin, CoordMetadata, metadata_manager_factory import iris.coords -from iris.exceptions import IrisIgnoringBoundsWarning +from iris.exceptions import IrisIgnoringBoundsWarning, warn_once_at_level class AuxCoordFactory(CFVariableMixin, metaclass=ABCMeta): @@ -429,7 +428,9 @@ def _check_dependencies(pressure_at_top, sigma, surface_air_pressure): f"Coordinate '{coord.name()}' has bounds. These will " "be disregarded" ) - warnings.warn(msg, category=IrisIgnoringBoundsWarning, stacklevel=2) + warn_once_at_level( + msg, category=IrisIgnoringBoundsWarning, stacklevel=2 + ) # Check units if sigma.units.is_unknown(): @@ -502,7 +503,7 @@ def make_coord(self, coord_dims_func): if sigma.shape[-1:] not in ok_bound_shapes: raise ValueError("Invalid sigma coordinate bounds") if pressure_at_top.shape[-1:] not in [(), (1,)]: - warnings.warn( + warn_once_at_level( "Pressure at top coordinate has bounds. These are being " "disregarded", category=IrisIgnoringBoundsWarning, @@ -511,7 +512,7 @@ def make_coord(self, coord_dims_func): bds_shape = list(pressure_at_top_pts.shape) + [1] pressure_at_top = pressure_at_top_pts.reshape(bds_shape) if surface_air_pressure.shape[-1:] not in [(), (1,)]: - warnings.warn( + warn_once_at_level( "Surface pressure coordinate has bounds. These are being " "disregarded", category=IrisIgnoringBoundsWarning, @@ -572,7 +573,7 @@ def __init__(self, delta=None, sigma=None, orography=None): "Orography coordinate {!r} has bounds." " These will be disregarded.".format(orography.name()) ) - warnings.warn(msg, category=IrisIgnoringBoundsWarning, stacklevel=2) + warn_once_at_level(msg, category=IrisIgnoringBoundsWarning, stacklevel=2) self.delta = delta self.sigma = sigma @@ -648,7 +649,7 @@ def make_coord(self, coord_dims_func): if sigma.shape[-1:] not in ok_bound_shapes: raise ValueError("Invalid sigma coordinate bounds.") if orography.shape[-1:] not in [(), (1,)]: - warnings.warn( + warn_once_at_level( "Orography coordinate has bounds. These are being disregarded.", category=IrisIgnoringBoundsWarning, stacklevel=2, @@ -704,7 +705,9 @@ def update(self, old_coord, new_coord=None): "Orography coordinate {!r} has bounds." " These will be disregarded.".format(new_coord.name()) ) - warnings.warn(msg, category=IrisIgnoringBoundsWarning, stacklevel=2) + warn_once_at_level( + msg, category=IrisIgnoringBoundsWarning, stacklevel=2 + ) self.orography = new_coord @@ -768,7 +771,7 @@ def _check_dependencies(delta, sigma, surface_air_pressure): "Surface pressure coordinate {!r} has bounds. These will" " be disregarded.".format(surface_air_pressure.name()) ) - warnings.warn(msg, category=IrisIgnoringBoundsWarning, stacklevel=2) + warn_once_at_level(msg, category=IrisIgnoringBoundsWarning, stacklevel=2) # Check units. if sigma is not None and sigma.units.is_unknown(): @@ -854,7 +857,7 @@ def make_coord(self, coord_dims_func): if sigma.shape[-1:] not in ok_bound_shapes: raise ValueError("Invalid sigma coordinate bounds.") if surface_air_pressure.shape[-1:] not in [(), (1,)]: - warnings.warn( + warn_once_at_level( "Surface pressure coordinate has bounds. " "These are being disregarded.", category=IrisIgnoringBoundsWarning, @@ -968,7 +971,9 @@ def _check_dependencies(sigma, eta, depth, depth_c, nsigma, zlev): "The {} coordinate {!r} has bounds. " "These are being disregarded.".format(term, coord.name()) ) - warnings.warn(msg, category=IrisIgnoringBoundsWarning, stacklevel=2) + warn_once_at_level( + msg, category=IrisIgnoringBoundsWarning, stacklevel=2 + ) for coord, term in ((depth_c, "depth_c"), (nsigma, "nsigma")): if coord is not None and coord.shape != (1,): @@ -1125,7 +1130,9 @@ def make_coord(self, coord_dims_func): "The {} coordinate {!r} has bounds. " "These are being disregarded.".format(key, name) ) - warnings.warn(msg, category=IrisIgnoringBoundsWarning, stacklevel=2) + warn_once_at_level( + msg, category=IrisIgnoringBoundsWarning, stacklevel=2 + ) # Swap bounds with points. bds_shape = list(nd_points_by_key[key].shape) + [1] bounds = nd_points_by_key[key].reshape(bds_shape) @@ -1205,7 +1212,9 @@ def _check_dependencies(sigma, eta, depth): "The {} coordinate {!r} has bounds. " "These are being disregarded.".format(term, coord.name()) ) - warnings.warn(msg, category=IrisIgnoringBoundsWarning, stacklevel=2) + warn_once_at_level( + msg, category=IrisIgnoringBoundsWarning, stacklevel=2 + ) # Check units. if sigma is not None and sigma.units.is_unknown(): @@ -1281,7 +1290,9 @@ def make_coord(self, coord_dims_func): "The {} coordinate {!r} has bounds. " "These are being disregarded.".format(key, name) ) - warnings.warn(msg, category=IrisIgnoringBoundsWarning, stacklevel=2) + warn_once_at_level( + msg, category=IrisIgnoringBoundsWarning, stacklevel=2 + ) # Swap bounds with points. bds_shape = list(nd_points_by_key[key].shape) + [1] bounds = nd_points_by_key[key].reshape(bds_shape) @@ -1372,7 +1383,9 @@ def _check_dependencies(s, c, eta, depth, depth_c): "The {} coordinate {!r} has bounds. " "These are being disregarded.".format(term, coord.name()) ) - warnings.warn(msg, category=IrisIgnoringBoundsWarning, stacklevel=2) + warn_once_at_level( + msg, category=IrisIgnoringBoundsWarning, stacklevel=2 + ) if depth_c is not None and depth_c.shape != (1,): msg = "Expected scalar {} coordinate {!r}: got shape {!r}.".format( @@ -1466,7 +1479,9 @@ def make_coord(self, coord_dims_func): "The {} coordinate {!r} has bounds. " "These are being disregarded.".format(key, name) ) - warnings.warn(msg, category=IrisIgnoringBoundsWarning, stacklevel=2) + warn_once_at_level( + msg, category=IrisIgnoringBoundsWarning, stacklevel=2 + ) # Swap bounds with points. bds_shape = list(nd_points_by_key[key].shape) + [1] bounds = nd_points_by_key[key].reshape(bds_shape) @@ -1561,7 +1576,9 @@ def _check_dependencies(s, eta, depth, a, b, depth_c): "The {} coordinate {!r} has bounds. " "These are being disregarded.".format(term, coord.name()) ) - warnings.warn(msg, category=IrisIgnoringBoundsWarning, stacklevel=2) + warn_once_at_level( + msg, category=IrisIgnoringBoundsWarning, stacklevel=2 + ) coords = ((a, "a"), (b, "b"), (depth_c, "depth_c")) for coord, term in coords: @@ -1658,7 +1675,9 @@ def make_coord(self, coord_dims_func): "The {} coordinate {!r} has bounds. " "These are being disregarded.".format(key, name) ) - warnings.warn(msg, category=IrisIgnoringBoundsWarning, stacklevel=2) + warn_once_at_level( + msg, category=IrisIgnoringBoundsWarning, stacklevel=2 + ) # Swap bounds with points. bds_shape = list(nd_points_by_key[key].shape) + [1] bounds = nd_points_by_key[key].reshape(bds_shape) @@ -1753,7 +1772,9 @@ def _check_dependencies(s, c, eta, depth, depth_c): "The {} coordinate {!r} has bounds. " "These are being disregarded.".format(term, coord.name()) ) - warnings.warn(msg, category=IrisIgnoringBoundsWarning, stacklevel=2) + warn_once_at_level( + msg, category=IrisIgnoringBoundsWarning, stacklevel=2 + ) if depth_c is not None and depth_c.shape != (1,): msg = "Expected scalar depth_c coordinate {!r}: got shape {!r}.".format( @@ -1847,7 +1868,9 @@ def make_coord(self, coord_dims_func): "The {} coordinate {!r} has bounds. " "These are being disregarded.".format(key, name) ) - warnings.warn(msg, category=IrisIgnoringBoundsWarning, stacklevel=2) + warn_once_at_level( + msg, category=IrisIgnoringBoundsWarning, stacklevel=2 + ) # Swap bounds with points. bds_shape = list(nd_points_by_key[key].shape) + [1] bounds = nd_points_by_key[key].reshape(bds_shape) diff --git a/lib/iris/config.py b/lib/iris/config.py index c617783dec..fd85d70ebb 100644 --- a/lib/iris/config.py +++ b/lib/iris/config.py @@ -29,9 +29,9 @@ import contextlib import logging import os.path -import warnings import iris.exceptions +from iris.exceptions import warn_once_at_level def get_logger(name, datefmt=None, fmt=None, level=None, propagate=None, handler=True): @@ -137,7 +137,7 @@ def get_dir_option(section, option, default=None): "Ignoring config item {!r}:{!r} (section:option) as {!r}" " is not a valid directory path." ) - warnings.warn( + warn_once_at_level( msg.format(section, option, c_path), category=iris.exceptions.IrisIgnoringWarning, ) @@ -244,7 +244,7 @@ def __setattr__(self, name, value): "Attempting to set invalid value {!r} for " "attribute {!r}. Defaulting to {!r}." ) - warnings.warn( + warn_once_at_level( wmsg.format(value, name, good_value), category=iris.exceptions.IrisDefaultingWarning, ) diff --git a/lib/iris/coord_systems.py b/lib/iris/coord_systems.py index 5b4e593d83..6014e12b53 100644 --- a/lib/iris/coord_systems.py +++ b/lib/iris/coord_systems.py @@ -7,13 +7,13 @@ from abc import ABCMeta, abstractmethod from functools import cached_property import re -import warnings import cartopy.crs as ccrs import numpy as np from iris._deprecation import warn_deprecated import iris.exceptions +from iris.exceptions import warn_once_at_level def _arg_default(value, default, cast_as=float): @@ -439,7 +439,7 @@ def inverse_flattening(self, value): "the GeogCS object. To change other properties set them explicitly" " or create a new GeogCS instance." ) - warnings.warn(wmsg, category=iris.exceptions.IrisUserWarning) + warn_once_at_level(wmsg, category=iris.exceptions.IrisUserWarning) value = float(value) self._inverse_flattening = value @@ -765,7 +765,7 @@ def __repr__(self): def as_cartopy_crs(self): globe = self._ellipsoid_to_globe(self.ellipsoid, ccrs.Globe()) - warnings.warn( + warn_once_at_level( "Discarding false_easting and false_northing that are " "not used by Cartopy.", category=iris.exceptions.IrisDefaultingWarning, diff --git a/lib/iris/coords.py b/lib/iris/coords.py index 8cbe0984cd..fe426932e2 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -11,7 +11,6 @@ from functools import lru_cache from itertools import zip_longest import operator -import warnings import zlib import dask.array as da @@ -30,6 +29,7 @@ metadata_manager_factory, ) import iris.exceptions +from iris.exceptions import warn_once_at_level import iris.time import iris.util @@ -1974,7 +1974,7 @@ def contiguous_bounds(self): """ if not self.has_bounds(): if self.ndim == 1: - warnings.warn( + warn_once_at_level( "Coordinate {!r} is not bounded, guessing " "contiguous bounds.".format(self.name()), category=iris.exceptions.IrisGuessBoundsWarning, @@ -2136,7 +2136,7 @@ def serialize(x): "Collapsing a multi-dimensional coordinate. " "Metadata may not be fully descriptive for {!r}." ) - warnings.warn( + warn_once_at_level( msg.format(self.name()), category=iris.exceptions.IrisVagueMetadataWarning, ) @@ -2149,7 +2149,7 @@ def serialize(x): "Metadata may not be fully descriptive for {!r}. " "Ignoring bounds." ) - warnings.warn( + warn_once_at_level( msg.format(str(exc), self.name()), category=iris.exceptions.IrisVagueMetadataWarning, ) @@ -2160,7 +2160,7 @@ def serialize(x): "Collapsing a non-contiguous coordinate. " "Metadata may not be fully descriptive for {!r}." ) - warnings.warn( + warn_once_at_level( msg.format(self.name()), category=iris.exceptions.IrisVagueMetadataWarning, ) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index a6f283100a..04173b8375 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -20,7 +20,6 @@ Optional, Union, ) -import warnings from xml.dom.minidom import Document import zlib @@ -44,6 +43,7 @@ import iris.coord_systems import iris.coords import iris.exceptions +from iris.exceptions import warn_once_at_level import iris.util __all__ = ["Cube", "CubeAttrsDict", "CubeList"] @@ -4040,7 +4040,7 @@ def collapsed(self, coords, aggregator, **kwargs): lat_match = [coord for coord in coords if "latitude" in coord.name()] if lat_match: for coord in lat_match: - warnings.warn( + warn_once_at_level( msg.format(coord.name()), category=iris.exceptions.IrisUserWarning, ) @@ -4593,7 +4593,7 @@ def rolling_window(self, coord, aggregator, window, **kwargs): # now update all of the coordinates to reflect the aggregation for coord_ in self.coords(dimensions=dimension): if coord_.has_bounds(): - warnings.warn( + warn_once_at_level( "The bounds of coordinate %r were ignored in " "the rolling window operation." % coord_.name(), category=iris.exceptions.IrisIgnoringBoundsWarning, diff --git a/lib/iris/exceptions.py b/lib/iris/exceptions.py index 0fb06f3b26..6a96d04384 100644 --- a/lib/iris/exceptions.py +++ b/lib/iris/exceptions.py @@ -3,6 +3,9 @@ # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. """Exceptions specific to the Iris package.""" +from functools import lru_cache +import traceback +import warnings class IrisError(Exception): @@ -344,3 +347,18 @@ class IrisSaverFillValueWarning(IrisMaskValueMatchWarning, IrisSaveWarning): """ pass + + +@lru_cache(128) +def warn_once(msg, stacklevel, frame, **kwargs): + """Raise a warning only if a similar one has not been raised before.""" + warnings.warn(msg, stacklevel=stacklevel, **kwargs) + + +def warn_once_at_level(msg, stacklevel=0, **kwargs): + """Raise a warning only if a similar one hasn't been raised from the same line + for a given stack level. + """ + stacklevel += 1 + frame = traceback.format_stack()[-stacklevel] + warn_once(msg, stacklevel + 1, frame, **kwargs) diff --git a/lib/iris/experimental/regrid.py b/lib/iris/experimental/regrid.py index 1bea933fbf..5a523f5349 100644 --- a/lib/iris/experimental/regrid.py +++ b/lib/iris/experimental/regrid.py @@ -18,7 +18,6 @@ """ import copy import functools -import warnings import cartopy.crs as ccrs import numpy as np @@ -37,7 +36,7 @@ import iris.analysis.cartography import iris.coord_systems import iris.cube -from iris.exceptions import IrisImpossibleUpdateWarning +from iris.exceptions import IrisImpossibleUpdateWarning, warn_once_at_level from iris.util import _meshgrid wmsg = ( @@ -517,7 +516,7 @@ def regrid_reference_surface( "Cannot update aux_factory {!r} because of dropped" " coordinates.".format(factory.name()) ) - warnings.warn(msg, category=IrisImpossibleUpdateWarning) + warn_once_at_level(msg, category=IrisImpossibleUpdateWarning) return result def __call__(self, src_cube): diff --git a/lib/iris/experimental/ugrid/cf.py b/lib/iris/experimental/ugrid/cf.py index d00fd6ef24..688791abab 100644 --- a/lib/iris/experimental/ugrid/cf.py +++ b/lib/iris/experimental/ugrid/cf.py @@ -8,7 +8,7 @@ Eventual destination: :mod:`iris.fileformats.cf`. """ -import warnings +from iris.exceptions import warn_once_at_level from ...exceptions import IrisCfLabelVarWarning, IrisCfMissingVarWarning from ...fileformats import cf @@ -65,7 +65,9 @@ def identify(cls, variables, ignore=None, target=None, warn=True): f"{nc_var_name}" ) if warn: - warnings.warn(message, category=IrisCfMissingVarWarning) + warn_once_at_level( + message, category=IrisCfMissingVarWarning + ) else: # Restrict to non-string type i.e. not a # CFLabelVariable. @@ -80,7 +82,7 @@ def identify(cls, variables, ignore=None, target=None, warn=True): f"CF-netCDF label variable." ) if warn: - warnings.warn( + warn_once_at_level( message, category=IrisCfLabelVarWarning ) @@ -139,7 +141,7 @@ def identify(cls, variables, ignore=None, target=None, warn=True): f"variable {nc_var_name}" ) if warn: - warnings.warn( + warn_once_at_level( message, category=IrisCfMissingVarWarning, ) @@ -158,7 +160,7 @@ def identify(cls, variables, ignore=None, target=None, warn=True): f"CF-netCDF label variable." ) if warn: - warnings.warn( + warn_once_at_level( message, category=IrisCfLabelVarWarning, ) @@ -217,7 +219,9 @@ def identify(cls, variables, ignore=None, target=None, warn=True): f"referenced by netCDF variable {nc_var_name}" ) if warn: - warnings.warn(message, category=IrisCfMissingVarWarning) + warn_once_at_level( + message, category=IrisCfMissingVarWarning + ) else: # Restrict to non-string type i.e. not a # CFLabelVariable. @@ -230,7 +234,9 @@ def identify(cls, variables, ignore=None, target=None, warn=True): f"variable." ) if warn: - warnings.warn(message, category=IrisCfLabelVarWarning) + warn_once_at_level( + message, category=IrisCfLabelVarWarning + ) return result diff --git a/lib/iris/experimental/ugrid/load.py b/lib/iris/experimental/ugrid/load.py index 8ba7448e8c..acd4d24a58 100644 --- a/lib/iris/experimental/ugrid/load.py +++ b/lib/iris/experimental/ugrid/load.py @@ -15,7 +15,8 @@ from itertools import groupby from pathlib import Path import threading -import warnings + +from iris.exceptions import warn_once_at_level from ...config import get_logger from ...coords import AuxCoord @@ -356,7 +357,7 @@ def _build_mesh(cf, mesh_var, file_path): cf_role_message = f"{mesh_var.cf_name} has an inappropriate cf_role: {cf_role}." if cf_role_message: cf_role_message += " Correcting to 'mesh_topology'." - warnings.warn( + warn_once_at_level( cf_role_message, category=_WarnComboCfDefaulting, ) @@ -377,7 +378,7 @@ def _build_mesh(cf, mesh_var, file_path): f" : *Assuming* topology_dimension={topology_dimension}" ", consistent with the attached connectivities." ) - warnings.warn(msg, category=_WarnComboCfDefaulting) + warn_once_at_level(msg, category=_WarnComboCfDefaulting) else: quoted_topology_dimension = mesh_var.topology_dimension if quoted_topology_dimension != topology_dimension: @@ -389,7 +390,7 @@ def _build_mesh(cf, mesh_var, file_path): f"{quoted_topology_dimension}" " -- ignoring this as it is inconsistent." ) - warnings.warn( + warn_once_at_level( msg, category=_WarnComboCfDefaultingIgnoring, ) diff --git a/lib/iris/fileformats/_ff.py b/lib/iris/fileformats/_ff.py index d2ce1bcefb..0e4c3e3d33 100644 --- a/lib/iris/fileformats/_ff.py +++ b/lib/iris/fileformats/_ff.py @@ -5,7 +5,6 @@ """Provides UK Met Office Fields File (FF) format specific capabilities.""" import os -import warnings import numpy as np @@ -13,6 +12,7 @@ IrisDefaultingWarning, IrisLoadWarning, NotYetImplementedError, + warn_once_at_level, ) from iris.fileformats._ff_cross_references import STASH_TRANS @@ -419,7 +419,7 @@ def grid(self): grid_class = self.GRID_STAGGERING_CLASS.get(self.grid_staggering) if grid_class is None: grid_class = NewDynamics - warnings.warn( + warn_once_at_level( "Staggered grid type: {} not currently interpreted, assuming " "standard C-grid".format(self.grid_staggering), category=_WarnComboLoadingDefaulting, @@ -539,7 +539,7 @@ def range_order(range1, range2, resolution): "may be incorrect, not having taken into account the " "boundary size." ) - warnings.warn(msg, category=IrisLoadWarning) + warn_once_at_level(msg, category=IrisLoadWarning) else: range2 = field_dim[0] - res_low range1 = field_dim[0] - halo_dim * res_low @@ -608,7 +608,7 @@ def _adjust_field_for_lbc(self, field): field.y = self._det_border(field.y, boundary_packing.y_halo) else: if field.bdy < 0: - warnings.warn( + warn_once_at_level( "The LBC has a bdy less than 0. No " "case has previously been seen of " "this, and the decompression may be " @@ -720,7 +720,7 @@ def _extract_field(self): else: subgrid = stash_entry.grid_code if subgrid not in HANDLED_GRIDS: - warnings.warn( + warn_once_at_level( "The stash code {} is on a grid {} " "which has not been explicitly " "handled by the fieldsfile loader." @@ -742,7 +742,7 @@ def _extract_field(self): "STASH to grid type mapping. Picking the P " "position as the cell type".format(stash) ) - warnings.warn( + warn_once_at_level( msg, category=_WarnComboLoadingDefaulting, ) @@ -751,7 +751,7 @@ def _extract_field(self): field.bplat = grid.pole_lat field.bplon = grid.pole_lon elif no_x or no_y: - warnings.warn( + warn_once_at_level( "Partially missing X or Y coordinate values.", category=IrisLoadWarning, ) @@ -799,7 +799,9 @@ def _extract_field(self): "Input field skipped as PPField creation failed :" " error = {!r}" ) - warnings.warn(msg.format(str(valerr)), category=IrisLoadWarning) + warn_once_at_level( + msg.format(str(valerr)), category=IrisLoadWarning + ) def __iter__(self): return pp._interpret_fields(self._extract_field()) diff --git a/lib/iris/fileformats/_nc_load_rules/actions.py b/lib/iris/fileformats/_nc_load_rules/actions.py index fefa58ad10..ccfa0c6668 100644 --- a/lib/iris/fileformats/_nc_load_rules/actions.py +++ b/lib/iris/fileformats/_nc_load_rules/actions.py @@ -40,10 +40,10 @@ """ from functools import wraps -import warnings from iris.config import get_logger import iris.exceptions +from iris.exceptions import warn_once_at_level import iris.fileformats.cf import iris.fileformats.pp as pp @@ -495,7 +495,7 @@ def action_formula_type(engine, formula_root_fact): succeed = False rule_name += f"(FAILED - unrecognised formula type = {formula_type!r})" msg = f"Ignored formula of unrecognised type: {formula_type!r}." - warnings.warn( + warn_once_at_level( msg, category=_WarnComboCfLoadIgnoring, ) @@ -513,7 +513,7 @@ def action_formula_type(engine, formula_root_fact): f"Formula of type ={formula_type!r} " f"overrides another of type ={existing_type!r}.)" ) - warnings.warn( + warn_once_at_level( msg, category=_WarnComboLoadIgnoring, ) diff --git a/lib/iris/fileformats/_nc_load_rules/helpers.py b/lib/iris/fileformats/_nc_load_rules/helpers.py index 98357c1f26..31f76512c6 100644 --- a/lib/iris/fileformats/_nc_load_rules/helpers.py +++ b/lib/iris/fileformats/_nc_load_rules/helpers.py @@ -15,7 +15,6 @@ """ import re from typing import List -import warnings import cf_units import numpy as np @@ -29,6 +28,7 @@ import iris.coord_systems import iris.coords import iris.exceptions +from iris.exceptions import warn_once_at_level import iris.fileformats.cf as cf import iris.fileformats.netcdf from iris.fileformats.netcdf.loader import _get_cf_var_data @@ -299,7 +299,7 @@ def _split_cell_methods(nc_cell_methods: str) -> List[re.Match]: "Cell methods may be incorrectly parsed due to mismatched " "brackets" ) - warnings.warn( + warn_once_at_level( msg, category=iris.exceptions.IrisCfLoadWarning, stacklevel=2, @@ -320,7 +320,9 @@ def _split_cell_methods(nc_cell_methods: str) -> List[re.Match]: nc_cell_method_match = _CM_PARSE.match(nc_cell_method_str.strip()) if not nc_cell_method_match: msg = f"Failed to fully parse cell method string: {nc_cell_methods}" - warnings.warn(msg, category=iris.exceptions.IrisCfLoadWarning, stacklevel=2) + warn_once_at_level( + msg, category=iris.exceptions.IrisCfLoadWarning, stacklevel=2 + ) continue nc_cell_methods_matches.append(nc_cell_method_match) @@ -371,7 +373,7 @@ def parse_cell_methods(nc_cell_methods, cf_name=None): name = "{}".format(cf_name) msg = msg.replace("variable", "variable {!r}".format(name)) else: - warnings.warn( + warn_once_at_level( msg, category=UnknownCellMethodWarning, ) @@ -435,7 +437,7 @@ def parse_cell_methods(nc_cell_methods, cf_name=None): cell_methods.append(cell_method) # only prints one warning, rather than each loop if msg: - warnings.warn(msg, category=UnknownCellMethodWarning) + warn_once_at_level(msg, category=UnknownCellMethodWarning) return tuple(cell_methods) @@ -474,7 +476,7 @@ def build_cube_metadata(engine): cube.attributes.globals[str(attr_name)] = attr_value except ValueError as e: msg = "Skipping disallowed global attribute {!r}: {}" - warnings.warn( + warn_once_at_level( msg.format(attr_name, str(e)), category=_WarnComboIgnoringLoad, ) @@ -520,7 +522,7 @@ def _get_ellipsoid(cf_grid_var): "applied. To apply the datum when loading, use the " "iris.FUTURE.datum_support flag." ) - warnings.warn(wmsg, category=FutureWarning, stacklevel=14) + iris.exceptions.warn_once_at_level(wmsg, category=FutureWarning, stacklevel=14) datum = None if datum is not None: @@ -549,7 +551,7 @@ def build_rotated_coordinate_system(engine, cf_grid_var): north_pole_latitude = getattr(cf_grid_var, CF_ATTR_GRID_NORTH_POLE_LAT, 90.0) north_pole_longitude = getattr(cf_grid_var, CF_ATTR_GRID_NORTH_POLE_LON, 0.0) if north_pole_latitude is None or north_pole_longitude is None: - warnings.warn( + warn_once_at_level( "Rotated pole position is not fully specified", category=iris.exceptions.IrisCfLoadWarning, ) @@ -899,7 +901,7 @@ def get_attr_units(cf_var, attributes): msg = "Ignoring netCDF variable {!r} invalid units {!r}".format( cf_var.cf_name, attr_units ) - warnings.warn( + warn_once_at_level( msg, category=_WarnComboIgnoringCfLoad, ) @@ -984,7 +986,7 @@ def get_cf_bounds_var(cf_coord_var): climatological = True if attr_bounds is not None and attr_climatology is not None: - warnings.warn( + warn_once_at_level( "Ignoring climatology in favour of bounds attribute " "on NetCDF variable {!r}.".format(cf_coord_var.cf_name), category=_WarnComboIgnoringCfLoad, @@ -1039,7 +1041,7 @@ def build_dimension_coordinate( if ma.is_masked(points_data): points_data = ma.filled(points_data) msg = "Gracefully filling {!r} dimension coordinate masked points" - warnings.warn( + warn_once_at_level( msg.format(str(cf_coord_var.cf_name)), category=_WarnComboDefaultingLoad, ) @@ -1052,7 +1054,7 @@ def build_dimension_coordinate( if ma.is_masked(bounds_data): bounds_data = ma.filled(bounds_data) msg = "Gracefully filling {!r} dimension coordinate masked bounds" - warnings.warn( + warn_once_at_level( msg.format(str(cf_coord_var.cf_name)), category=_WarnComboDefaultingLoad, ) @@ -1112,7 +1114,7 @@ def build_dimension_coordinate( "Failed to create {name!r} dimension coordinate: {error}\n" "Gracefully creating {name!r} auxiliary coordinate instead." ) - warnings.warn( + warn_once_at_level( msg.format(name=str(cf_coord_var.cf_name), error=e_msg), category=_WarnComboDefaultingCfLoad, ) @@ -1130,7 +1132,7 @@ def build_dimension_coordinate( try: cube.add_aux_coord(coord, data_dims) except iris.exceptions.CannotAddError as e_msg: - warnings.warn( + warn_once_at_level( coord_skipped_msg.format(error=e_msg), category=iris.exceptions.IrisCannotAddWarning, ) @@ -1144,7 +1146,7 @@ def build_dimension_coordinate( # Scalar coords are placed in the aux_coords container. cube.add_aux_coord(coord, data_dims) except iris.exceptions.CannotAddError as e_msg: - warnings.warn( + warn_once_at_level( coord_skipped_msg.format(error=e_msg), category=iris.exceptions.IrisCannotAddWarning, ) @@ -1218,7 +1220,7 @@ def build_auxiliary_coordinate( cube.add_aux_coord(coord, data_dims) except iris.exceptions.CannotAddError as e_msg: msg = "{name!r} coordinate not added to Cube: {error}" - warnings.warn( + warn_once_at_level( msg.format(name=str(cf_coord_var.cf_name), error=e_msg), category=iris.exceptions.IrisCannotAddWarning, ) @@ -1270,7 +1272,7 @@ def build_cell_measures(engine, cf_cm_var): cube.add_cell_measure(cell_measure, data_dims) except iris.exceptions.CannotAddError as e_msg: msg = "{name!r} cell measure not added to Cube: {error}" - warnings.warn( + warn_once_at_level( msg.format(name=str(cf_cm_var.cf_name), error=e_msg), category=iris.exceptions.IrisCannotAddWarning, ) @@ -1318,7 +1320,7 @@ def build_ancil_var(engine, cf_av_var): cube.add_ancillary_variable(av, data_dims) except iris.exceptions.CannotAddError as e_msg: msg = "{name!r} ancillary variable not added to Cube: {error}" - warnings.warn( + warn_once_at_level( msg.format(name=str(cf_av_var.cf_name), error=e_msg), category=iris.exceptions.IrisCannotAddWarning, ) @@ -1519,7 +1521,7 @@ def has_supported_mercator_parameters(engine, cf_name): ) if scale_factor_at_projection_origin is not None and standard_parallel is not None: - warnings.warn( + warn_once_at_level( "It does not make sense to provide both " '"scale_factor_at_projection_origin" and "standard_parallel".', category=iris.exceptions.IrisCfInvalidCoordParamWarning, @@ -1550,14 +1552,14 @@ def has_supported_polar_stereographic_parameters(engine, cf_name): ) if latitude_of_projection_origin != 90 and latitude_of_projection_origin != -90: - warnings.warn( + warn_once_at_level( '"latitude_of_projection_origin" must be +90 or -90.', category=iris.exceptions.IrisCfInvalidCoordParamWarning, ) is_valid = False if scale_factor_at_projection_origin is not None and standard_parallel is not None: - warnings.warn( + warn_once_at_level( "It does not make sense to provide both " '"scale_factor_at_projection_origin" and "standard_parallel".', category=iris.exceptions.IrisCfInvalidCoordParamWarning, @@ -1565,7 +1567,7 @@ def has_supported_polar_stereographic_parameters(engine, cf_name): is_valid = False if scale_factor_at_projection_origin is None and standard_parallel is None: - warnings.warn( + warn_once_at_level( 'One of "scale_factor_at_projection_origin" and ' '"standard_parallel" is required.', category=iris.exceptions.IrisCfInvalidCoordParamWarning, diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index 5a0230d5eb..ceabac62f8 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -18,12 +18,12 @@ from collections.abc import Iterable, MutableMapping import os import re -import warnings import numpy as np import numpy.ma as ma import iris.exceptions +from iris.exceptions import warn_once_at_level from iris.fileformats.netcdf import _thread_safe_nc import iris.util @@ -276,7 +276,7 @@ def identify(cls, variables, ignore=None, target=None, warn=True): if name not in variables: if warn: message = "Missing CF-netCDF ancillary data variable %r, referenced by netCDF variable %r" - warnings.warn( + warn_once_at_level( message % (name, nc_var_name), category=iris.exceptions.IrisCfMissingVarWarning, ) @@ -325,7 +325,7 @@ def identify(cls, variables, ignore=None, target=None, warn=True): if name not in variables: if warn: message = "Missing CF-netCDF auxiliary coordinate variable %r, referenced by netCDF variable %r" - warnings.warn( + warn_once_at_level( message % (name, nc_var_name), category=iris.exceptions.IrisCfMissingVarWarning, ) @@ -375,7 +375,7 @@ def identify(cls, variables, ignore=None, target=None, warn=True): if name not in variables: if warn: message = "Missing CF-netCDF boundary variable %r, referenced by netCDF variable %r" - warnings.warn( + warn_once_at_level( message % (name, nc_var_name), category=iris.exceptions.IrisCfMissingVarWarning, ) @@ -451,7 +451,7 @@ def identify(cls, variables, ignore=None, target=None, warn=True): if name not in variables: if warn: message = "Missing CF-netCDF climatology variable %r, referenced by netCDF variable %r" - warnings.warn( + warn_once_at_level( message % (name, nc_var_name), category=iris.exceptions.IrisCfMissingVarWarning, ) @@ -591,7 +591,7 @@ def identify(cls, variables, ignore=None, target=None, warn=True): if variable_name not in variables: if warn: message = "Missing CF-netCDF formula term variable %r, referenced by netCDF variable %r" - warnings.warn( + warn_once_at_level( message % (variable_name, nc_var_name), category=iris.exceptions.IrisCfMissingVarWarning, ) @@ -658,7 +658,7 @@ def identify(cls, variables, ignore=None, target=None, warn=True): if name not in variables: if warn: message = "Missing CF-netCDF grid mapping variable %r, referenced by netCDF variable %r" - warnings.warn( + warn_once_at_level( message % (name, nc_var_name), category=iris.exceptions.IrisCfMissingVarWarning, ) @@ -699,7 +699,7 @@ def identify(cls, variables, ignore=None, target=None, warn=True): if name not in variables: if warn: message = "Missing CF-netCDF label variable %r, referenced by netCDF variable %r" - warnings.warn( + warn_once_at_level( message % (name, nc_var_name), category=iris.exceptions.IrisCfMissingVarWarning, ) @@ -874,7 +874,7 @@ def identify(cls, variables, ignore=None, target=None, warn=True): if variable_name not in variables: if warn: message = "Missing CF-netCDF measure variable %r, referenced by netCDF variable %r" - warnings.warn( + warn_once_at_level( message % (variable_name, nc_var_name), category=iris.exceptions.IrisCfMissingVarWarning, ) @@ -1080,7 +1080,7 @@ def __init__(self, file_source, warn=False, monotonic=False): "NETCDF3_CLASSIC", "NETCDF3_64BIT", ]: - warnings.warn( + warn_once_at_level( "Optimise CF-netCDF loading by converting data from NetCDF3 " 'to NetCDF4 file format using the "nccopy" command.', category=iris.exceptions.IrisLoadWarning, @@ -1219,7 +1219,7 @@ def _build(cf_variable): cf_variable.dimensions, ) ) - warnings.warn( + warn_once_at_level( msg, category=iris.exceptions.IrisCfNonSpanningVarWarning, ) @@ -1268,7 +1268,7 @@ def _build(cf_variable): cf_variable.dimensions, ) ) - warnings.warn( + warn_once_at_level( msg, category=iris.exceptions.IrisCfNonSpanningVarWarning, ) diff --git a/lib/iris/fileformats/name_loaders.py b/lib/iris/fileformats/name_loaders.py index 3e337383cb..f6d1f414c3 100644 --- a/lib/iris/fileformats/name_loaders.py +++ b/lib/iris/fileformats/name_loaders.py @@ -8,7 +8,6 @@ import datetime from operator import itemgetter import re -import warnings import cf_units import numpy as np @@ -16,7 +15,7 @@ import iris.coord_systems from iris.coords import AuxCoord, CellMethod, DimCoord import iris.cube -from iris.exceptions import IrisLoadWarning, TranslationError +from iris.exceptions import IrisLoadWarning, TranslationError, warn_once_at_level import iris.util EARTH_RADIUS = 6371229.0 @@ -265,7 +264,9 @@ def _parse_units(units): try: units = cf_units.Unit(units) except ValueError: - warnings.warn("Unknown units: {!r}".format(units), category=IrisLoadWarning) + warn_once_at_level( + "Unknown units: {!r}".format(units), category=IrisLoadWarning + ) units = cf_units.Unit(None) return units @@ -595,7 +596,7 @@ def _build_cell_methods(av_or_ints, coord): else: cell_method = None msg = "Unknown {} statistic: {!r}. Unable to create cell method." - warnings.warn(msg.format(coord, av_or_int), category=IrisLoadWarning) + warn_once_at_level(msg.format(coord, av_or_int), category=IrisLoadWarning) cell_methods.append(cell_method) # NOTE: this can be a None return cell_methods diff --git a/lib/iris/fileformats/netcdf/loader.py b/lib/iris/fileformats/netcdf/loader.py index 42b6ee509c..7142ea9cdd 100644 --- a/lib/iris/fileformats/netcdf/loader.py +++ b/lib/iris/fileformats/netcdf/loader.py @@ -16,7 +16,6 @@ from enum import Enum, auto import threading from typing import Union -import warnings import numpy as np @@ -35,6 +34,7 @@ import iris.coord_systems import iris.coords import iris.exceptions +from iris.exceptions import warn_once_at_level import iris.fileformats.cf from iris.fileformats.netcdf import _thread_safe_nc from iris.fileformats.netcdf.saver import _CF_ATTRS @@ -413,7 +413,7 @@ def coord_from_term(term): for coord, cf_var_name in engine.cube_parts["coordinates"]: if cf_var_name == name: return coord - warnings.warn( + warn_once_at_level( "Unable to find coordinate for variable {!r}".format(name), category=iris.exceptions.IrisFactoryCoordNotFoundWarning, ) @@ -454,7 +454,7 @@ def coord_from_term(term): "Ignoring atmosphere hybrid sigma pressure " "scalar coordinate {!r} bounds.".format(coord_p0.name()) ) - warnings.warn( + warn_once_at_level( msg, category=_WarnComboIgnoringBoundsLoad, ) @@ -644,7 +644,7 @@ def load_cubes(file_sources, callback=None, constraints=None): try: _load_aux_factory(engine, cube) except ValueError as e: - warnings.warn( + warn_once_at_level( "{}".format(e), category=iris.exceptions.IrisLoadWarning, ) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index 086732e597..366c17c2b7 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -20,7 +20,6 @@ import re import string from typing import List -import warnings import cf_units import dask @@ -45,6 +44,7 @@ import iris.coords from iris.coords import AncillaryVariable, AuxCoord, CellMeasure, DimCoord import iris.exceptions +from iris.exceptions import warn_once_at_level import iris.fileformats.cf from iris.fileformats.netcdf import _dask_locks, _thread_safe_nc import iris.io @@ -373,7 +373,7 @@ def _fillvalue_report(fill_info, is_masked, contains_fill_value, warn=False): ) if warn and result is not None: - warnings.warn( + warn_once_at_level( result, category=_WarnComboMaskSave, ) @@ -737,7 +737,7 @@ def write( cf_patch(profile, self._dataset, cf_var_cube) else: msg = "cf_profile is available but no {} defined.".format("cf_patch") - warnings.warn(msg, category=iris.exceptions.IrisCfSaveWarning) + warn_once_at_level(msg, category=iris.exceptions.IrisCfSaveWarning) @staticmethod def check_attribute_compliance(container, data_dtype): @@ -1133,7 +1133,7 @@ def _add_aux_factories(self, cube, cf_var_cube, dimension_names): msg = "Unable to determine formula terms for AuxFactory: {!r}".format( factory ) - warnings.warn(msg, category=iris.exceptions.IrisSaveWarning) + warn_once_at_level(msg, category=iris.exceptions.IrisSaveWarning) else: # Override `standard_name`, `long_name`, and `axis` of the # primary coord that signals the presence of a dimensionless @@ -2082,7 +2082,7 @@ def add_ellipsoid(ellipsoid): # osgb (a specific tmerc) elif isinstance(cs, iris.coord_systems.OSGB): - warnings.warn( + warn_once_at_level( "OSGB coordinate system not yet handled", category=iris.exceptions.IrisSaveWarning, ) @@ -2168,7 +2168,7 @@ def add_ellipsoid(ellipsoid): # other else: - warnings.warn( + warn_once_at_level( "Unable to represent the horizontal " "coordinate system. The coordinate system " "type %r is not yet implemented." % type(cs), @@ -2342,7 +2342,7 @@ def set_packing_ncattrs(cfvar): "attribute, but {attr_name!r} should only be a CF " "global attribute.".format(attr_name=attr_name) ) - warnings.warn(msg, category=iris.exceptions.IrisCfSaveWarning) + warn_once_at_level(msg, category=iris.exceptions.IrisCfSaveWarning) _setncattr(cf_var, attr_name, value) @@ -2565,7 +2565,9 @@ def complete(self, issue_warnings=True) -> List[Warning]: if issue_warnings: # Issue any delayed warnings from the compute. for delayed_warning in result_warnings: - warnings.warn(delayed_warning, category=iris.exceptions.IrisSaveWarning) + warn_once_at_level( + delayed_warning, category=iris.exceptions.IrisSaveWarning + ) return result_warnings @@ -2811,7 +2813,7 @@ def attr_values_equal(val1, val2): } if invalid_globals: # Some cubes have different global attributes: modify cubes as required. - warnings.warn( + warn_once_at_level( f"Saving the cube global attributes {sorted(invalid_globals)} as local " "(i.e. data-variable) attributes, where possible, since they are not " "the same on all input cubes.", @@ -2827,7 +2829,7 @@ def attr_values_equal(val1, val2): # Catch any demoted attrs where there is already a local version blocked_attrs = demote_attrs & set(cube.attributes.locals) if blocked_attrs: - warnings.warn( + warn_once_at_level( f"Global cube attributes {sorted(blocked_attrs)} " f'of cube "{cube.name()}" were not saved, overlaid ' "by existing local attributes with the same names.", @@ -2973,7 +2975,7 @@ def is_valid_packspec(p): msg = "cf_profile is available but no {} defined.".format( "cf_patch_conventions" ) - warnings.warn(msg, category=iris.exceptions.IrisCfSaveWarning) + warn_once_at_level(msg, category=iris.exceptions.IrisCfSaveWarning) # Add conventions attribute. if iris.FUTURE.save_split_attrs: diff --git a/lib/iris/fileformats/nimrod_load_rules.py b/lib/iris/fileformats/nimrod_load_rules.py index 5ca9ef4be3..521dbf2400 100644 --- a/lib/iris/fileformats/nimrod_load_rules.py +++ b/lib/iris/fileformats/nimrod_load_rules.py @@ -6,7 +6,6 @@ import re import string -import warnings import cf_units import cftime @@ -19,6 +18,7 @@ CoordinateNotFoundError, IrisNimrodTranslationWarning, TranslationError, + warn_once_at_level, ) __all__ = ["run"] @@ -179,7 +179,7 @@ def units(cube, field): cube.units = field_units except ValueError: # Just add it as an attribute. - warnings.warn( + warn_once_at_level( "Unhandled units '{0}' recorded in cube attributes.".format(field_units), category=IrisNimrodTranslationWarning, ) @@ -398,7 +398,7 @@ def coord_system(field, handle_metadata_errors): field.tm_meridian_scaling, ) if any([is_missing(field, v) for v in crs_args]): - warnings.warn( + warn_once_at_level( "Coordinate Reference System is not completely defined. " "Plotting and reprojection may be impaired.", category=IrisNimrodTranslationWarning, @@ -524,7 +524,7 @@ def vertical_coord(cube, field): f"{field.vertical_coord_type} != {field.reference_vertical_coord_type}. " f"Assuming {field.vertical_coord_type}" ) - warnings.warn(msg, category=IrisNimrodTranslationWarning) + warn_once_at_level(msg, category=IrisNimrodTranslationWarning) coord_point = field.vertical_coord if coord_point == 8888.0: @@ -564,7 +564,7 @@ def vertical_coord(cube, field): cube.add_aux_coord(new_coord) return - warnings.warn( + warn_once_at_level( "Vertical coord {!r} not yet handled".format(field.vertical_coord_type), category=TranslationWarning, ) @@ -803,7 +803,7 @@ def probability_coord(cube, field, handle_metadata_errors): "standard_name", coord_keys.get("long_name", coord_keys.get("var_name", None)), ) - warnings.warn( + warn_once_at_level( f"No default units for {coord_name} coord of {cube.name()}. " "Meta-data may be incomplete.", category=IrisNimrodTranslationWarning, diff --git a/lib/iris/fileformats/pp.py b/lib/iris/fileformats/pp.py index 2780c52625..868ea09ef4 100644 --- a/lib/iris/fileformats/pp.py +++ b/lib/iris/fileformats/pp.py @@ -11,7 +11,6 @@ import os import re import struct -import warnings import cf_units import cftime @@ -24,6 +23,7 @@ import iris.config import iris.coord_systems import iris.exceptions +from iris.exceptions import warn_once_at_level # NOTE: this is for backwards-compatitibility *ONLY* # We could simply remove it for v2.0 ? @@ -1154,7 +1154,7 @@ def save(self, file_handle): "missing data. To save these as normal values, please " "set the field BMDI not equal to any valid data points." ) - warnings.warn( + warn_once_at_level( msg.format(mdi), category=_WarnComboLoadingMask, ) @@ -1271,7 +1271,7 @@ def save(self, file_handle): if data.dtype == np.dtype(">f4"): lb[self.HEADER_DICT["lbuser"][0]] = 1 elif data.dtype == np.dtype(">f8"): - warnings.warn( + warn_once_at_level( "Downcasting array precision from float64 to float32" " for save.If float64 precision is required then" " please save in a different format", @@ -1694,7 +1694,7 @@ def _interpret_fields(fields): # they were encountered before the landmask reference field. if landmask_compressed_fields: if land_mask_field is None: - warnings.warn( + warn_once_at_level( "Landmask compressed fields existed without a " "landmask to decompress with. The data will have " "a shape of (0, 0) and will not read.", @@ -1862,7 +1862,7 @@ def _field_gen(filename, read_data_bytes, little_ended=False): "Unable to interpret field {}. {}. Skipping " "the remainder of the file.".format(field_count, str(e)) ) - warnings.warn( + warn_once_at_level( msg, category=_WarnComboIgnoringLoad, ) @@ -1882,7 +1882,7 @@ def _field_gen(filename, read_data_bytes, little_ended=False): "after the header in the file ({} and {}). " "Skipping the remainder of the file." ) - warnings.warn( + warn_once_at_level( wmsg.format( pp_field.lblrec * PP_WORD_DEPTH, len_of_data_plus_extra ), diff --git a/lib/iris/fileformats/pp_save_rules.py b/lib/iris/fileformats/pp_save_rules.py index 20ed0bd618..04aedd18bb 100644 --- a/lib/iris/fileformats/pp_save_rules.py +++ b/lib/iris/fileformats/pp_save_rules.py @@ -5,13 +5,11 @@ """PP Save Rules.""" -import warnings - import cftime import iris from iris.aux_factory import HybridHeightFactory, HybridPressureFactory -from iris.exceptions import IrisPpClimModifiedWarning +from iris.exceptions import IrisPpClimModifiedWarning, warn_once_at_level from iris.fileformats._ff_cross_references import STASH_TRANS from iris.fileformats._pp_lbproc_pairs import LBPROC_MAP from iris.fileformats.rules import ( @@ -904,4 +902,4 @@ def verify(cube, field): def _conditional_warning(condition, warning): if condition: - warnings.warn(warning, category=IrisPpClimModifiedWarning) + warn_once_at_level(warning, category=IrisPpClimModifiedWarning) diff --git a/lib/iris/fileformats/rules.py b/lib/iris/fileformats/rules.py index aa3d4696bc..d167fb2804 100644 --- a/lib/iris/fileformats/rules.py +++ b/lib/iris/fileformats/rules.py @@ -5,13 +5,13 @@ """Generalised mechanisms for metadata translation and cube construction.""" import collections -import warnings import cf_units from iris.analysis import Linear import iris.cube import iris.exceptions +from iris.exceptions import warn_once_at_level import iris.fileformats.um_cf_map Factory = collections.namedtuple("Factory", ["factory_class", "args"]) @@ -40,7 +40,7 @@ def as_cube(self): # time-varying surface pressure in hybrid-presure. src_cubes = src_cubes.merge(unique=False) if len(src_cubes) > 1: - warnings.warn( + warn_once_at_level( "Multiple reference cubes for {}".format(self.name), category=iris.exceptions.IrisUserWarning, ) @@ -313,7 +313,7 @@ def _make_cube(field, converter): cube.units = metadata.units except ValueError: msg = "Ignoring PP invalid units {!r}".format(metadata.units) - warnings.warn(msg, category=iris.exceptions.IrisIgnoringWarning) + warn_once_at_level(msg, category=iris.exceptions.IrisIgnoringWarning) cube.attributes["invalid_units"] = metadata.units cube.units = cf_units._UNKNOWN_UNIT_STRING @@ -334,7 +334,7 @@ def _resolve_factory_references( except _ReferenceError as e: msg = "Unable to create instance of {factory}. " + str(e) factory_name = factory.factory_class.__name__ - warnings.warn( + warn_once_at_level( msg.format(factory=factory_name), category=iris.exceptions.IrisUserWarning, ) diff --git a/lib/iris/iterate.py b/lib/iris/iterate.py index be2a436a5e..f726fac74a 100644 --- a/lib/iris/iterate.py +++ b/lib/iris/iterate.py @@ -6,11 +6,10 @@ from collections.abc import Iterator import itertools -import warnings import numpy as np -from iris.exceptions import IrisUserWarning +from iris.exceptions import IrisUserWarning, warn_once_at_level __all__ = ["izip"] @@ -155,7 +154,7 @@ def izip(*cubes, **kwargs): "step." % coord_a.name() ) if coord_a != coord_b: - warnings.warn( + warn_once_at_level( "Iterating over coordinate '%s' in step whose " "definitions match but whose values " "differ." % coord_a.name(), diff --git a/lib/iris/pandas.py b/lib/iris/pandas.py index d60b46011e..b6ad373aa1 100644 --- a/lib/iris/pandas.py +++ b/lib/iris/pandas.py @@ -9,7 +9,6 @@ """ import datetime from itertools import chain, combinations -import warnings import cf_units from cf_units import Unit @@ -18,6 +17,8 @@ import numpy.ma as ma import pandas as pd +from iris.exceptions import warn_once_at_level + try: from pandas.core.indexes.datetimes import DatetimeIndex # pandas >=0.20 except ImportError: @@ -430,7 +431,7 @@ def format_dimensional_metadata(dm_class_, values_, name_, dimensions_): if columns_ignored: ignored_args = ", ".join([t[2] for t in class_arg_mapping]) message = f"The input pandas_structure is a Series; ignoring arguments: {ignored_args} ." - warnings.warn(message, category=IrisIgnoringWarning) + warn_once_at_level(message, category=IrisIgnoringWarning) class_arg_mapping = [] non_data_names = [] @@ -870,7 +871,7 @@ def merge_metadata(meta_var_list): "'iris.FUTURE.pandas_ndim = True'. More info is in the " "documentation." ) - warnings.warn(message, category=FutureWarning) + warn_once_at_level(message, category=FutureWarning) # The legacy behaviour. data = cube.data diff --git a/lib/iris/plot.py b/lib/iris/plot.py index c727607449..811bf5c099 100644 --- a/lib/iris/plot.py +++ b/lib/iris/plot.py @@ -10,7 +10,6 @@ import collections import datetime -import warnings import cartopy.crs as ccrs from cartopy.geodesic import Geodesic @@ -30,7 +29,11 @@ import iris.coord_systems import iris.coords import iris.cube -from iris.exceptions import IrisError, IrisUnsupportedPlottingWarning +from iris.exceptions import ( + IrisError, + IrisUnsupportedPlottingWarning, + warn_once_at_level, +) # Importing iris.palette to register the brewer palettes. import iris.palette @@ -1913,13 +1916,13 @@ def update_animation_iris(i, cubes, vmin, vmax, coords): if plot_func.__module__ not in supported: msg = 'Given plotting module "{}" may not be supported, intended ' "use: {}." msg = msg.format(plot_func.__module__, supported) - warnings.warn(msg, category=IrisUnsupportedPlottingWarning) + warn_once_at_level(msg, category=IrisUnsupportedPlottingWarning) supported = ["contour", "contourf", "pcolor", "pcolormesh"] if plot_func.__name__ not in supported: msg = 'Given plotting function "{}" may not be supported, intended ' "use: {}." msg = msg.format(plot_func.__name__, supported) - warnings.warn(msg, category=IrisUnsupportedPlottingWarning) + warn_once_at_level(msg, category=IrisUnsupportedPlottingWarning) # Determine plot range. vmin = kwargs.pop("vmin", min([cc.data.min() for cc in cubes])) diff --git a/lib/iris/tests/integration/netcdf/test_general.py b/lib/iris/tests/integration/netcdf/test_general.py index 751c160805..945c8be4b9 100644 --- a/lib/iris/tests/integration/netcdf/test_general.py +++ b/lib/iris/tests/integration/netcdf/test_general.py @@ -505,7 +505,7 @@ def test_datum_once(self): fnames = [ "false_east_north_merc.nc", "non_unit_scale_factor_merc.nc", - # toa_brightness_temperature.nc, + "toa_brightness_temperature.nc", ] fpaths = [ tests.get_data_path(("NetCDF", "mercator", fname)) for fname in fnames @@ -518,6 +518,13 @@ def test_datum_once(self): warnings.warn("Dummy warning", category=iris.exceptions.IrisUserWarning) assert len(record) == 2 + with warnings.catch_warnings(record=True) as record: + warnings.simplefilter("default") + iris.load_cube(fpaths[0]) + iris.load_cube(fpaths[1]) + iris.load_cube(fpaths[2]) + assert len(record) == 3 + if __name__ == "__main__": tests.main()