From 4f126a0bcb52851a6f28667da65d43d0af89a252 Mon Sep 17 00:00:00 2001 From: Martin Yeo <40734014+trexfeathers@users.noreply.github.com> Date: Fri, 22 Sep 2023 11:30:09 +0100 Subject: [PATCH] Categorise warnings (#5498) * Introduce IrisUserWarning. * Align existing warning subcategories. * Plant a flag for future DeprecationWarning use. * Introduce test_categorised_warnings(). * Fix backwards compatibility of existing warnings classes. * Categorise all Iris warnings. * Fix test_categorised_warnings() by using ast module. * Add missing warning category kwargs. * Warnings combo experiment. * Warnings combo finalise. * Fix stray comma in coords.py. * Fix failing tests. * Categorise warning tests. * What's New entry. --- docs/src/whatsnew/latest.rst | 8 +- lib/iris/__init__.py | 2 + lib/iris/_concatenate.py | 3 +- lib/iris/_deprecation.py | 10 +- lib/iris/analysis/_regrid.py | 3 +- lib/iris/analysis/calculus.py | 6 +- lib/iris/analysis/cartography.py | 23 +- lib/iris/analysis/geometry.py | 8 +- lib/iris/analysis/maths.py | 3 +- lib/iris/aux_factory.py | 68 ++++-- lib/iris/config.py | 12 +- lib/iris/coord_systems.py | 7 +- lib/iris/coords.py | 18 +- lib/iris/cube.py | 8 +- lib/iris/exceptions.py | 209 ++++++++++++++++++ lib/iris/experimental/regrid.py | 3 +- lib/iris/experimental/ugrid/cf.py | 27 ++- lib/iris/experimental/ugrid/load.py | 31 ++- lib/iris/fileformats/_ff.py | 35 ++- .../fileformats/_nc_load_rules/actions.py | 29 ++- .../fileformats/_nc_load_rules/helpers.py | 130 +++++++++-- lib/iris/fileformats/cf.py | 50 ++++- lib/iris/fileformats/name_loaders.py | 10 +- lib/iris/fileformats/netcdf/loader.py | 22 +- lib/iris/fileformats/netcdf/saver.py | 41 +++- lib/iris/fileformats/nimrod_load_rules.py | 26 ++- lib/iris/fileformats/pp.py | 47 +++- lib/iris/fileformats/pp_save_rules.py | 3 +- lib/iris/fileformats/rules.py | 10 +- lib/iris/iterate.py | 5 +- lib/iris/pandas.py | 5 +- lib/iris/plot.py | 6 +- lib/iris/tests/graphics/idiff.py | 3 +- .../experimental/test_ugrid_load.py | 9 +- .../integration/netcdf/test_delayed_save.py | 8 +- .../tests/integration/netcdf/test_general.py | 6 +- .../netcdf/test_self_referencing.py | 9 +- lib/iris/tests/integration/test_pp.py | 6 +- lib/iris/tests/test_coding_standards.py | 61 +++++ lib/iris/tests/test_concatenate.py | 13 +- lib/iris/tests/test_coordsystem.py | 3 +- lib/iris/tests/test_hybrid.py | 9 +- lib/iris/tests/test_iterate.py | 5 +- lib/iris/tests/test_netcdf.py | 5 +- .../unit/analysis/cartography/test_project.py | 4 +- .../geometry/test_geometry_area_weights.py | 5 +- lib/iris/tests/unit/coords/test_Coord.py | 14 +- lib/iris/tests/unit/cube/test_Cube.py | 12 +- ...test_CFUGridAuxiliaryCoordinateVariable.py | 14 +- .../cf/test_CFUGridConnectivityVariable.py | 14 +- .../ugrid/cf/test_CFUGridMeshVariable.py | 14 +- .../tests/unit/fileformats/ff/test_FF2PP.py | 4 +- .../unit/fileformats/ff/test_FFHeader.py | 5 +- .../name_loaders/test__build_cell_methods.py | 5 +- .../nc_load_rules/actions/__init__.py | 3 +- .../helpers/test_parse_cell_methods.py | 5 +- .../netcdf/loader/test__load_aux_factory.py | 4 +- .../fileformats/netcdf/saver/test_Saver.py | 7 +- .../saver/test_Saver__lazy_stream_data.py | 3 +- .../netcdf/saver/test__fillvalue_report.py | 8 +- .../nimrod_load_rules/test_vertical_coord.py | 2 +- .../tests/unit/fileformats/pp/test_PPField.py | 7 +- 62 files changed, 930 insertions(+), 205 deletions(-) diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index a9b470296f..4c732c43df 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -30,7 +30,9 @@ This document explains the changes made to Iris for this release ✨ Features =========== -#. N/A +#. `@trexfeathers`_ and `@HGWright`_ (reviewer) sub-categorised all Iris' + :class:`UserWarning`\s for richer filtering. The full index of + sub-categories can be seen here: :mod:`iris.exceptions` . (:pull:`5498`) 🐛 Bugs Fixed @@ -38,7 +40,7 @@ This document explains the changes made to Iris for this release #. `@scottrobinson02`_ fixed the output units when dividing a coordinate by a cube. (:issue:`5305`, :pull:`5331`) - + #. `@ESadek-MO`_ has updated :mod:`iris.tests.graphics.idiff` to stop duplicated file names preventing acceptance. (:issue:`5098`, :pull:`5482`) @@ -87,7 +89,7 @@ This document explains the changes made to Iris for this release #. `@trexfeathers`_ replaced all uses of the ``logging.WARNING`` level, in favour of using Python warnings, following team agreement. (:pull:`5488`) - + #. `@trexfeathers`_ adapted benchmarking to work with ASV ``>=v0.6`` by no longer using the ``--strict`` argument. (:pull:`5496`) diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index 0e6670533f..2a3bd8a753 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -175,6 +175,8 @@ def __init__(self, datum_support=False, pandas_ndim=False): # self.__dict__['example_future_flag'] = example_future_flag self.__dict__["datum_support"] = datum_support self.__dict__["pandas_ndim"] = pandas_ndim + # TODO: next major release: set IrisDeprecation to subclass + # DeprecationWarning instead of UserWarning. def __repr__(self): # msg = ('Future(example_future_flag={})') diff --git a/lib/iris/_concatenate.py b/lib/iris/_concatenate.py index c6d58b1622..837afd73f3 100644 --- a/lib/iris/_concatenate.py +++ b/lib/iris/_concatenate.py @@ -16,6 +16,7 @@ import iris.coords import iris.cube +import iris.exceptions from iris.util import array_equal, guess_coord_axis # @@ -998,7 +999,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) + warnings.warn(msg, category=iris.exceptions.IrisUserWarning) # Check for compatible AuxCoords. if match: diff --git a/lib/iris/_deprecation.py b/lib/iris/_deprecation.py index 73fcedcd82..8ad762a558 100644 --- a/lib/iris/_deprecation.py +++ b/lib/iris/_deprecation.py @@ -12,7 +12,13 @@ class IrisDeprecation(UserWarning): - """An Iris deprecation warning.""" + """ + An Iris deprecation warning. + + Note this subclasses UserWarning for backwards compatibility with Iris' + original deprection warnings. Should subclass DeprecationWarning at the + next major release. + """ pass @@ -44,7 +50,7 @@ def warn_deprecated(msg, stacklevel=2): >>> """ - warnings.warn(msg, IrisDeprecation, stacklevel=stacklevel) + warnings.warn(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 4592a0ede7..65679cd968 100644 --- a/lib/iris/analysis/_regrid.py +++ b/lib/iris/analysis/_regrid.py @@ -20,6 +20,7 @@ snapshot_grid, ) from iris.analysis._scipy_interpolate import _RegularGridInterpolator +from iris.exceptions import IrisImpossibleUpdateWarning from iris.util import _meshgrid, guess_coord_axis @@ -1136,6 +1137,6 @@ def regrid_reference_surface( "Cannot update aux_factory {!r} because of dropped" " coordinates.".format(factory.name()) ) - warnings.warn(msg) + warnings.warn(msg, category=IrisImpossibleUpdateWarning) return result diff --git a/lib/iris/analysis/calculus.py b/lib/iris/analysis/calculus.py index 75b7d86406..44b1adc580 100644 --- a/lib/iris/analysis/calculus.py +++ b/lib/iris/analysis/calculus.py @@ -24,6 +24,7 @@ import iris.analysis.maths import iris.coord_systems import iris.coords +from iris.exceptions import IrisUserWarning from iris.util import delta __all__ = ["cube_delta", "curl", "differentiate"] @@ -85,7 +86,10 @@ def _construct_midpoint_coord(coord, circular=None): "Construction coordinate midpoints for the '{}' coordinate, " "though it has the attribute 'circular'={}." ) - warnings.warn(msg.format(circular, coord.circular, coord.name())) + warnings.warn( + msg.format(circular, coord.circular, coord.name()), + category=IrisUserWarning, + ) if coord.ndim != 1: raise iris.exceptions.CoordinateMultiDimError(coord) diff --git a/lib/iris/analysis/cartography.py b/lib/iris/analysis/cartography.py index 0d17f0b38a..0fae5bc499 100644 --- a/lib/iris/analysis/cartography.py +++ b/lib/iris/analysis/cartography.py @@ -401,16 +401,25 @@ 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("Assuming spherical earth from ellipsoid.") + warnings.warn( + "Assuming spherical earth from ellipsoid.", + category=iris.exceptions.IrisDefaultingWarning, + ) radius_of_earth = cs.semi_major_axis elif isinstance(cs, iris.coord_systems.RotatedGeogCS) and ( cs.ellipsoid is not None ): if cs.ellipsoid.inverse_flattening != 0.0: - warnings.warn("Assuming spherical earth from ellipsoid.") + warnings.warn( + "Assuming spherical earth from ellipsoid.", + category=iris.exceptions.IrisDefaultingWarning, + ) radius_of_earth = cs.ellipsoid.semi_major_axis else: - warnings.warn("Using DEFAULT_SPHERICAL_EARTH_RADIUS.") + warnings.warn( + "Using DEFAULT_SPHERICAL_EARTH_RADIUS.", + category=iris.exceptions.IrisDefaultingWarning, + ) radius_of_earth = DEFAULT_SPHERICAL_EARTH_RADIUS # Get the lon and lat coords and axes @@ -551,7 +560,7 @@ def cosine_latitude_weights(cube): warnings.warn( "Out of range latitude values will be " "clipped to the valid range.", - UserWarning, + category=iris.exceptions.IrisDefaultingWarning, ) points = lat.points l_weights = np.cos(points).clip(0.0, 1.0) @@ -665,7 +674,8 @@ def project(cube, target_proj, nx=None, ny=None): # Assume WGS84 latlon if unspecified warnings.warn( "Coordinate system of latitude and longitude " - "coordinates is not specified. Assuming WGS84 Geodetic." + "coordinates is not specified. Assuming WGS84 Geodetic.", + category=iris.exceptions.IrisDefaultingWarning, ) orig_cs = iris.coord_systems.GeogCS( semi_major_axis=6378137.0, inverse_flattening=298.257223563 @@ -857,7 +867,8 @@ def project(cube, target_proj, nx=None, ny=None): lat_coord.name(), lon_coord.name(), [coord.name() for coord in discarded_coords], - ) + ), + category=iris.exceptions.IrisIgnoringWarning, ) # TODO handle derived coords/aux_factories diff --git a/lib/iris/analysis/geometry.py b/lib/iris/analysis/geometry.py index b246b518d4..9898f4e974 100644 --- a/lib/iris/analysis/geometry.py +++ b/lib/iris/analysis/geometry.py @@ -74,7 +74,7 @@ def _extract_relevant_cube_slice(cube, geometry): except ValueError: warnings.warn( "The geometry exceeds the cube's x dimension at the " "lower end.", - UserWarning, + category=iris.exceptions.IrisGeometryExceedWarning, ) x_min_ix = 0 if x_ascending else x_coord.points.size - 1 @@ -84,7 +84,7 @@ def _extract_relevant_cube_slice(cube, geometry): except ValueError: warnings.warn( "The geometry exceeds the cube's x dimension at the " "upper end.", - UserWarning, + category=iris.exceptions.IrisGeometryExceedWarning, ) x_max_ix = x_coord.points.size - 1 if x_ascending else 0 @@ -94,7 +94,7 @@ def _extract_relevant_cube_slice(cube, geometry): except ValueError: warnings.warn( "The geometry exceeds the cube's y dimension at the " "lower end.", - UserWarning, + category=iris.exceptions.IrisGeometryExceedWarning, ) y_min_ix = 0 if y_ascending else y_coord.points.size - 1 @@ -104,7 +104,7 @@ def _extract_relevant_cube_slice(cube, geometry): except ValueError: warnings.warn( "The geometry exceeds the cube's y dimension at the " "upper end.", - UserWarning, + category=iris.exceptions.IrisGeometryExceedWarning, ) y_max_ix = y_coord.points.size - 1 if y_ascending else 0 diff --git a/lib/iris/analysis/maths.py b/lib/iris/analysis/maths.py index b77c6cd80f..5e180c6ee2 100644 --- a/lib/iris/analysis/maths.py +++ b/lib/iris/analysis/maths.py @@ -988,7 +988,8 @@ def _broadcast_cube_coord_data(cube, other, operation_name, dim=None): if other.has_bounds(): warnings.warn( "Using {!r} with a bounded coordinate is not well " - "defined; ignoring bounds.".format(operation_name) + "defined; ignoring bounds.".format(operation_name), + category=iris.exceptions.IrisIgnoringBoundsWarning, ) points = other.points diff --git a/lib/iris/aux_factory.py b/lib/iris/aux_factory.py index f49de62b3f..323c89e3fb 100644 --- a/lib/iris/aux_factory.py +++ b/lib/iris/aux_factory.py @@ -21,6 +21,7 @@ metadata_manager_factory, ) import iris.coords +from iris.exceptions import IrisIgnoringBoundsWarning class AuxCoordFactory(CFVariableMixin, metaclass=ABCMeta): @@ -441,7 +442,9 @@ def _check_dependencies(pressure_at_top, sigma, surface_air_pressure): f"Coordinate '{coord.name()}' has bounds. These will " "be disregarded" ) - warnings.warn(msg, UserWarning, stacklevel=2) + warnings.warn( + msg, category=IrisIgnoringBoundsWarning, stacklevel=2 + ) # Check units if sigma.units.is_unknown(): @@ -522,7 +525,8 @@ def make_coord(self, coord_dims_func): if pressure_at_top.shape[-1:] not in [(), (1,)]: warnings.warn( "Pressure at top coordinate has bounds. These are being " - "disregarded" + "disregarded", + category=IrisIgnoringBoundsWarning, ) pressure_at_top_pts = nd_points_by_key["pressure_at_top"] bds_shape = list(pressure_at_top_pts.shape) + [1] @@ -530,7 +534,8 @@ def make_coord(self, coord_dims_func): if surface_air_pressure.shape[-1:] not in [(), (1,)]: warnings.warn( "Surface pressure coordinate has bounds. These are being " - "disregarded" + "disregarded", + category=IrisIgnoringBoundsWarning, ) surface_air_pressure_pts = nd_points_by_key[ "surface_air_pressure" @@ -595,7 +600,9 @@ def __init__(self, delta=None, sigma=None, orography=None): "Orography coordinate {!r} has bounds." " These will be disregarded.".format(orography.name()) ) - warnings.warn(msg, UserWarning, stacklevel=2) + warnings.warn( + msg, category=IrisIgnoringBoundsWarning, stacklevel=2 + ) self.delta = delta self.sigma = sigma @@ -684,7 +691,7 @@ def make_coord(self, coord_dims_func): warnings.warn( "Orography coordinate has bounds. " "These are being disregarded.", - UserWarning, + category=IrisIgnoringBoundsWarning, stacklevel=2, ) orography_pts = nd_points_by_key["orography"] @@ -739,7 +746,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, UserWarning, stacklevel=2) + warnings.warn( + msg, category=IrisIgnoringBoundsWarning, stacklevel=2 + ) self.orography = new_coord @@ -806,7 +815,9 @@ 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, UserWarning, stacklevel=2) + warnings.warn( + msg, category=IrisIgnoringBoundsWarning, stacklevel=2 + ) # Check units. if sigma is not None and sigma.units.is_unknown(): @@ -898,7 +909,8 @@ def make_coord(self, coord_dims_func): if surface_air_pressure.shape[-1:] not in [(), (1,)]: warnings.warn( "Surface pressure coordinate has bounds. " - "These are being disregarded." + "These are being disregarded.", + category=IrisIgnoringBoundsWarning, ) surface_air_pressure_pts = nd_points_by_key[ "surface_air_pressure" @@ -1012,7 +1024,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, UserWarning, stacklevel=2) + warnings.warn( + msg, category=IrisIgnoringBoundsWarning, stacklevel=2 + ) for coord, term in ((depth_c, "depth_c"), (nsigma, "nsigma")): if coord is not None and coord.shape != (1,): @@ -1187,7 +1201,9 @@ def make_coord(self, coord_dims_func): "The {} coordinate {!r} has bounds. " "These are being disregarded.".format(key, name) ) - warnings.warn(msg, UserWarning, stacklevel=2) + warnings.warn( + 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) @@ -1268,7 +1284,9 @@ def _check_dependencies(sigma, eta, depth): "The {} coordinate {!r} has bounds. " "These are being disregarded.".format(term, coord.name()) ) - warnings.warn(msg, UserWarning, stacklevel=2) + warnings.warn( + msg, category=IrisIgnoringBoundsWarning, stacklevel=2 + ) # Check units. if sigma is not None and sigma.units.is_unknown(): @@ -1349,7 +1367,9 @@ def make_coord(self, coord_dims_func): "The {} coordinate {!r} has bounds. " "These are being disregarded.".format(key, name) ) - warnings.warn(msg, UserWarning, stacklevel=2) + warnings.warn( + 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) @@ -1444,7 +1464,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, UserWarning, stacklevel=2) + warnings.warn( + msg, category=IrisIgnoringBoundsWarning, stacklevel=2 + ) if depth_c is not None and depth_c.shape != (1,): msg = ( @@ -1543,7 +1565,9 @@ def make_coord(self, coord_dims_func): "The {} coordinate {!r} has bounds. " "These are being disregarded.".format(key, name) ) - warnings.warn(msg, UserWarning, stacklevel=2) + warnings.warn( + 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) @@ -1637,7 +1661,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, UserWarning, stacklevel=2) + warnings.warn( + msg, category=IrisIgnoringBoundsWarning, stacklevel=2 + ) coords = ((a, "a"), (b, "b"), (depth_c, "depth_c")) for coord, term in coords: @@ -1740,7 +1766,9 @@ def make_coord(self, coord_dims_func): "The {} coordinate {!r} has bounds. " "These are being disregarded.".format(key, name) ) - warnings.warn(msg, UserWarning, stacklevel=2) + warnings.warn( + 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) @@ -1839,7 +1867,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, UserWarning, stacklevel=2) + warnings.warn( + msg, category=IrisIgnoringBoundsWarning, stacklevel=2 + ) if depth_c is not None and depth_c.shape != (1,): msg = ( @@ -1938,7 +1968,9 @@ def make_coord(self, coord_dims_func): "The {} coordinate {!r} has bounds. " "These are being disregarded.".format(key, name) ) - warnings.warn(msg, UserWarning, stacklevel=2) + warnings.warn( + 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 79d141e53f..03d3d363a6 100644 --- a/lib/iris/config.py +++ b/lib/iris/config.py @@ -36,6 +36,8 @@ import os.path import warnings +import iris.exceptions + def get_logger( name, datefmt=None, fmt=None, level=None, propagate=None, handler=True @@ -145,7 +147,10 @@ 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(msg.format(section, option, c_path)) + warnings.warn( + msg.format(section, option, c_path), + category=iris.exceptions.IrisIgnoringWarning, + ) return path @@ -251,7 +256,10 @@ def __setattr__(self, name, value): "Attempting to set invalid value {!r} for " "attribute {!r}. Defaulting to {!r}." ) - warnings.warn(wmsg.format(value, name, good_value)) + warnings.warn( + wmsg.format(value, name, good_value), + category=iris.exceptions.IrisDefaultingWarning, + ) value = good_value self.__dict__[name] = value diff --git a/lib/iris/coord_systems.py b/lib/iris/coord_systems.py index edf0c1871b..e2003d1286 100644 --- a/lib/iris/coord_systems.py +++ b/lib/iris/coord_systems.py @@ -15,6 +15,8 @@ import cartopy.crs as ccrs import numpy as np +import iris.exceptions + def _arg_default(value, default, cast_as=float): """Apply a default value and type for an optional kwarg.""" @@ -449,7 +451,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, UserWarning) + warnings.warn(wmsg, category=iris.exceptions.IrisUserWarning) value = float(value) self._inverse_flattening = value @@ -818,7 +820,8 @@ def as_cartopy_crs(self): warnings.warn( "Discarding false_easting and false_northing that are " - "not used by Cartopy." + "not used by Cartopy.", + category=iris.exceptions.IrisDefaultingWarning, ) return ccrs.Orthographic( diff --git a/lib/iris/coords.py b/lib/iris/coords.py index 1a6e8d4e6a..3ff9bc8e5e 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -2057,7 +2057,8 @@ def contiguous_bounds(self): if self.ndim == 1: warnings.warn( "Coordinate {!r} is not bounded, guessing " - "contiguous bounds.".format(self.name()) + "contiguous bounds.".format(self.name()), + category=iris.exceptions.IrisGuessBoundsWarning, ) bounds = self._guess_bounds() elif self.ndim == 2: @@ -2224,7 +2225,10 @@ def serialize(x): "Collapsing a multi-dimensional coordinate. " "Metadata may not be fully descriptive for {!r}." ) - warnings.warn(msg.format(self.name())) + warnings.warn( + msg.format(self.name()), + category=iris.exceptions.IrisVagueMetadataWarning, + ) else: try: self._sanity_check_bounds() @@ -2234,7 +2238,10 @@ def serialize(x): "Metadata may not be fully descriptive for {!r}. " "Ignoring bounds." ) - warnings.warn(msg.format(str(exc), self.name())) + warnings.warn( + msg.format(str(exc), self.name()), + category=iris.exceptions.IrisVagueMetadataWarning, + ) self.bounds = None else: if not self.is_contiguous(): @@ -2242,7 +2249,10 @@ def serialize(x): "Collapsing a non-contiguous coordinate. " "Metadata may not be fully descriptive for {!r}." ) - warnings.warn(msg.format(self.name())) + warnings.warn( + msg.format(self.name()), + category=iris.exceptions.IrisVagueMetadataWarning, + ) if self.has_bounds(): item = self.core_bounds() diff --git a/lib/iris/cube.py b/lib/iris/cube.py index aec80dce47..60fdbc9c94 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -3857,7 +3857,10 @@ def collapsed(self, coords, aggregator, **kwargs): ] if lat_match: for coord in lat_match: - warnings.warn(msg.format(coord.name())) + warnings.warn( + msg.format(coord.name()), + category=iris.exceptions.IrisUserWarning, + ) # Determine the dimensions we need to collapse (and those we don't) if aggregator.cell_method == "peak": @@ -4444,7 +4447,8 @@ def rolling_window(self, coord, aggregator, window, **kwargs): if coord_.has_bounds(): warnings.warn( "The bounds of coordinate %r were ignored in " - "the rolling window operation." % coord_.name() + "the rolling window operation." % coord_.name(), + category=iris.exceptions.IrisIgnoringBoundsWarning, ) if coord_.ndim != 1: diff --git a/lib/iris/exceptions.py b/lib/iris/exceptions.py index 5d3da3349e..919917a01d 100644 --- a/lib/iris/exceptions.py +++ b/lib/iris/exceptions.py @@ -180,3 +180,212 @@ class CannotAddError(ValueError): """Raised when an object (e.g. coord) cannot be added to a :class:`~iris.cube.Cube`.""" pass + + +############################################################################### +# WARNINGS +# Please namespace all warning objects (i.e. prefix with Iris...). + + +class IrisUserWarning(UserWarning): + """ + Base class for :class:`UserWarning`\\ s generated by Iris. + """ + + pass + + +class IrisLoadWarning(IrisUserWarning): + """Any warning relating to loading.""" + + pass + + +class IrisSaveWarning(IrisUserWarning): + """Any warning relating to saving.""" + + pass + + +class IrisCfWarning(IrisUserWarning): + """Any warning relating to :term:`CF Conventions` .""" + + pass + + +class IrisIgnoringWarning(IrisUserWarning): + """ + Any warning that involves an Iris operation not using some information. + + E.g. :class:`~iris.aux_factory.AuxCoordFactory` generation disregarding + bounds. + """ + + pass + + +class IrisDefaultingWarning(IrisUserWarning): + """ + Any warning that involves Iris changing invalid/missing information. + + E.g. creating a :class:`~iris.coords.AuxCoord` from an invalid + :class:`~iris.coords.DimCoord` definition. + """ + + pass + + +class IrisVagueMetadataWarning(IrisUserWarning): + """Warnings where object metadata may not be fully descriptive.""" + + pass + + +class IrisUnsupportedPlottingWarning(IrisUserWarning): + """Warnings where support for a plotting module/function is not guaranteed.""" + + pass + + +class IrisImpossibleUpdateWarning(IrisUserWarning): + """ + Warnings where it is not possible to update an object. + + Mainly generated during regridding where the necessary information for + updating an :class:`~iris.aux_factory.AuxCoordFactory` is no longer + present. + """ + + pass + + +class IrisGeometryExceedWarning(IrisUserWarning): + """:mod:`iris.analysis.geometry` warnings about geometry exceeding dimensions.""" + + pass + + +class IrisMaskValueMatchWarning(IrisUserWarning): + """Warnings where the value representing masked data is actually present in data.""" + + pass + + +######## + + +class IrisCfLoadWarning(IrisCfWarning, IrisLoadWarning): + """Any warning relating to both loading and :term:`CF Conventions` .""" + + pass + + +class IrisCfSaveWarning(IrisCfWarning, IrisSaveWarning): + """Any warning relating to both saving and :term:`CF Conventions` .""" + + pass + + +class IrisCfInvalidCoordParamWarning(IrisCfLoadWarning): + """ + Warnings where incorrect information for CF coord construction is in a file. + """ + + pass + + +class IrisCfMissingVarWarning(IrisCfLoadWarning): + """ + Warnings where a CF variable references another variable that is not in the file. + """ + + pass + + +class IrisCfLabelVarWarning(IrisCfLoadWarning, IrisIgnoringWarning): + """ + Warnings where a CF string/label variable is being used inappropriately. + """ + + pass + + +class IrisCfNonSpanningVarWarning(IrisCfLoadWarning, IrisIgnoringWarning): + """ + Warnings where a CF variable is ignored because it does not span the required dimension. + """ + + pass + + +######## + + +class IrisIgnoringBoundsWarning(IrisIgnoringWarning): + """ + Warnings where bounds information has not been used by an Iris operation. + """ + + pass + + +class IrisCannotAddWarning(IrisIgnoringWarning): + """ + Warnings where a member object cannot be added to a :class:`~iris.cube.Cube` . + """ + + pass + + +class IrisGuessBoundsWarning(IrisDefaultingWarning): + """ + Warnings where Iris has filled absent bounds information with a best estimate. + """ + + pass + + +class IrisPpClimModifiedWarning(IrisSaveWarning, IrisDefaultingWarning): + """ + Warnings where a climatology has been modified while saving :term:`Post Processing (PP) Format` . + """ + + pass + + +class IrisFactoryCoordNotFoundWarning(IrisLoadWarning): + """ + Warnings where a referenced factory coord can not be found when loading a variable in :term:`NetCDF Format`. + """ + + pass + + +class IrisNimrodTranslationWarning(IrisLoadWarning): + """ + For unsupported vertical coord types in :mod:`iris.file_formats.nimrod_load_rules`. + + (Pre-dates the full categorisation of Iris UserWarnings). + """ + + pass + + +class IrisUnknownCellMethodWarning(IrisCfLoadWarning): + """ + If a loaded :class:`~iris.coords.CellMethod` is not one the method names known to Iris. + + (Pre-dates the full categorisation of Iris UserWarnings). + """ + + pass + + +class IrisSaverFillValueWarning(IrisMaskValueMatchWarning, IrisSaveWarning): + """ + For fill value complications during Iris file saving :term:`NetCDF Format`. + + (Pre-dates the full categorisation of Iris UserWarnings). + """ + + pass diff --git a/lib/iris/experimental/regrid.py b/lib/iris/experimental/regrid.py index 76c6002d2b..d5fa7c6f72 100644 --- a/lib/iris/experimental/regrid.py +++ b/lib/iris/experimental/regrid.py @@ -43,6 +43,7 @@ import iris.analysis.cartography import iris.coord_systems import iris.cube +from iris.exceptions import IrisImpossibleUpdateWarning from iris.util import _meshgrid wmsg = ( @@ -538,7 +539,7 @@ def regrid_reference_surface( "Cannot update aux_factory {!r} because of dropped" " coordinates.".format(factory.name()) ) - warnings.warn(msg) + warnings.warn(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 86b76c7a75..42c1cfd0a3 100644 --- a/lib/iris/experimental/ugrid/cf.py +++ b/lib/iris/experimental/ugrid/cf.py @@ -12,6 +12,7 @@ """ import warnings +from ...exceptions import IrisCfLabelVarWarning, IrisCfMissingVarWarning from ...fileformats import cf from .mesh import Connectivity @@ -65,7 +66,9 @@ def identify(cls, variables, ignore=None, target=None, warn=True): f"{nc_var_name}" ) if warn: - warnings.warn(message) + warnings.warn( + message, category=IrisCfMissingVarWarning + ) else: # Restrict to non-string type i.e. not a # CFLabelVariable. @@ -80,7 +83,9 @@ def identify(cls, variables, ignore=None, target=None, warn=True): f"CF-netCDF label variable." ) if warn: - warnings.warn(message) + warnings.warn( + message, category=IrisCfLabelVarWarning + ) return result @@ -136,7 +141,10 @@ def identify(cls, variables, ignore=None, target=None, warn=True): f"variable {nc_var_name}" ) if warn: - warnings.warn(message) + warnings.warn( + message, + category=IrisCfMissingVarWarning, + ) else: # Restrict to non-string type i.e. not a # CFLabelVariable. @@ -154,7 +162,10 @@ def identify(cls, variables, ignore=None, target=None, warn=True): f"CF-netCDF label variable." ) if warn: - warnings.warn(message) + warnings.warn( + message, + category=IrisCfLabelVarWarning, + ) return result @@ -211,7 +222,9 @@ def identify(cls, variables, ignore=None, target=None, warn=True): f"referenced by netCDF variable {nc_var_name}" ) if warn: - warnings.warn(message) + warnings.warn( + message, category=IrisCfMissingVarWarning + ) else: # Restrict to non-string type i.e. not a # CFLabelVariable. @@ -226,7 +239,9 @@ def identify(cls, variables, ignore=None, target=None, warn=True): f"variable." ) if warn: - warnings.warn(message) + warnings.warn( + message, category=IrisCfLabelVarWarning + ) return result diff --git a/lib/iris/experimental/ugrid/load.py b/lib/iris/experimental/ugrid/load.py index d2670ac690..67d1491930 100644 --- a/lib/iris/experimental/ugrid/load.py +++ b/lib/iris/experimental/ugrid/load.py @@ -19,6 +19,11 @@ from ...config import get_logger from ...coords import AuxCoord +from ...exceptions import ( + IrisCfWarning, + IrisDefaultingWarning, + IrisIgnoringWarning, +) from ...fileformats._nc_load_rules.helpers import get_attr_units, get_names from ...fileformats.netcdf import loader as nc_loader from ...io import decode_uri, expand_filespecs @@ -35,6 +40,20 @@ logger = get_logger(__name__, propagate=True, handler=False) +class _WarnComboCfDefaulting(IrisCfWarning, IrisDefaultingWarning): + """One-off combination of warning classes - enhances user filtering.""" + + pass + + +class _WarnComboCfDefaultingIgnoring( + _WarnComboCfDefaulting, IrisIgnoringWarning +): + """One-off combination of warning classes - enhances user filtering.""" + + pass + + class ParseUGridOnLoad(threading.local): def __init__(self): """ @@ -351,7 +370,10 @@ def _build_mesh(cf, mesh_var, file_path): ) if cf_role_message: cf_role_message += " Correcting to 'mesh_topology'." - warnings.warn(cf_role_message) + warnings.warn( + cf_role_message, + category=_WarnComboCfDefaulting, + ) if hasattr(mesh_var, "volume_node_connectivity"): topology_dimension = 3 @@ -369,7 +391,7 @@ def _build_mesh(cf, mesh_var, file_path): f" : *Assuming* topology_dimension={topology_dimension}" ", consistent with the attached connectivities." ) - warnings.warn(msg) + warnings.warn(msg, category=_WarnComboCfDefaulting) else: quoted_topology_dimension = mesh_var.topology_dimension if quoted_topology_dimension != topology_dimension: @@ -381,7 +403,10 @@ def _build_mesh(cf, mesh_var, file_path): f"{quoted_topology_dimension}" " -- ignoring this as it is inconsistent." ) - warnings.warn(msg) + warnings.warn( + msg, + category=_WarnComboCfDefaultingIgnoring, + ) node_dimension = None edge_dimension = getattr(mesh_var, "edge_dimension", None) diff --git a/lib/iris/fileformats/_ff.py b/lib/iris/fileformats/_ff.py index 2545bc39ae..5121b47976 100644 --- a/lib/iris/fileformats/_ff.py +++ b/lib/iris/fileformats/_ff.py @@ -13,7 +13,11 @@ import numpy as np -from iris.exceptions import NotYetImplementedError +from iris.exceptions import ( + IrisDefaultingWarning, + IrisLoadWarning, + NotYetImplementedError, +) from iris.fileformats._ff_cross_references import STASH_TRANS from . import pp @@ -118,6 +122,12 @@ REAL_POLE_LON = 5 +class _WarnComboLoadingDefaulting(IrisDefaultingWarning, IrisLoadWarning): + """One-off combination of warning classes - enhances user filtering.""" + + pass + + class Grid: """ An abstract class representing the default/file-level grid @@ -431,7 +441,8 @@ def grid(self): grid_class = NewDynamics warnings.warn( "Staggered grid type: {} not currently interpreted, assuming " - "standard C-grid".format(self.grid_staggering) + "standard C-grid".format(self.grid_staggering), + category=_WarnComboLoadingDefaulting, ) grid = grid_class( self.column_dependent_constants, @@ -554,7 +565,7 @@ def range_order(range1, range2, resolution): "may be incorrect, not having taken into account the " "boundary size." ) - warnings.warn(msg) + warnings.warn(msg, category=IrisLoadWarning) else: range2 = field_dim[0] - res_low range1 = field_dim[0] - halo_dim * res_low @@ -628,7 +639,8 @@ def _adjust_field_for_lbc(self, field): "The LBC has a bdy less than 0. No " "case has previously been seen of " "this, and the decompression may be " - "erroneous." + "erroneous.", + category=IrisLoadWarning, ) field.bzx -= field.bdx * boundary_packing.x_halo field.bzy -= field.bdy * boundary_packing.y_halo @@ -741,7 +753,8 @@ def _extract_field(self): "which has not been explicitly " "handled by the fieldsfile loader." " Assuming the data is on a P grid" - ".".format(stash, subgrid) + ".".format(stash, subgrid), + category=_WarnComboLoadingDefaulting, ) field.x, field.y = grid.vectors(subgrid) @@ -757,14 +770,18 @@ def _extract_field(self): "STASH to grid type mapping. Picking the P " "position as the cell type".format(stash) ) - warnings.warn(msg) + warnings.warn( + msg, + category=_WarnComboLoadingDefaulting, + ) field.bzx, field.bdx = grid.regular_x(subgrid) field.bzy, field.bdy = grid.regular_y(subgrid) field.bplat = grid.pole_lat field.bplon = grid.pole_lon elif no_x or no_y: warnings.warn( - "Partially missing X or Y coordinate values." + "Partially missing X or Y coordinate values.", + category=IrisLoadWarning, ) # Check for LBC fields. @@ -810,7 +827,9 @@ def _extract_field(self): "Input field skipped as PPField creation failed :" " error = {!r}" ) - warnings.warn(msg.format(str(valerr))) + warnings.warn( + 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 09237d3f11..be84b65132 100644 --- a/lib/iris/fileformats/_nc_load_rules/actions.py +++ b/lib/iris/fileformats/_nc_load_rules/actions.py @@ -44,6 +44,7 @@ import warnings from iris.config import get_logger +import iris.exceptions import iris.fileformats.cf import iris.fileformats.pp as pp @@ -53,6 +54,24 @@ logger = get_logger(__name__, fmt="[%(funcName)s]") +class _WarnComboCfLoadIgnoring( + iris.exceptions.IrisCfLoadWarning, + iris.exceptions.IrisIgnoringWarning, +): + """One-off combination of warning classes - enhances user filtering.""" + + pass + + +class _WarnComboLoadIgnoring( + iris.exceptions.IrisLoadWarning, + iris.exceptions.IrisIgnoringWarning, +): + """One-off combination of warning classes - enhances user filtering.""" + + pass + + def _default_rulenamesfunc(func_name): # A simple default function to deduce the rules-name from an action-name. funcname_prefix = "action_" @@ -471,7 +490,10 @@ 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(msg) + warnings.warn( + msg, + category=_WarnComboCfLoadIgnoring, + ) if succeed: # Check we don't already have one. existing_type = engine.requires.get("formula_type") @@ -486,7 +508,10 @@ 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(msg) + warnings.warn( + msg, + category=_WarnComboLoadIgnoring, + ) rule_name += f"_{formula_type}" # Set 'requires' info for iris.fileformats.netcdf._load_aux_factory. engine.requires["formula_type"] = formula_type diff --git a/lib/iris/fileformats/_nc_load_rules/helpers.py b/lib/iris/fileformats/_nc_load_rules/helpers.py index bbf9c660c5..19a9cd18ca 100644 --- a/lib/iris/fileformats/_nc_load_rules/helpers.py +++ b/lib/iris/fileformats/_nc_load_rules/helpers.py @@ -219,6 +219,42 @@ ] +class _WarnComboIgnoringLoad( + iris.exceptions.IrisIgnoringWarning, + iris.exceptions.IrisLoadWarning, +): + """One-off combination of warning classes - enhances user filtering.""" + + pass + + +class _WarnComboDefaultingLoad( + iris.exceptions.IrisDefaultingWarning, + iris.exceptions.IrisLoadWarning, +): + """One-off combination of warning classes - enhances user filtering.""" + + pass + + +class _WarnComboDefaultingCfLoad( + iris.exceptions.IrisCfLoadWarning, + iris.exceptions.IrisDefaultingWarning, +): + """One-off combination of warning classes - enhances user filtering.""" + + pass + + +class _WarnComboIgnoringCfLoad( + iris.exceptions.IrisIgnoringWarning, + iris.exceptions.IrisCfLoadWarning, +): + """One-off combination of warning classes - enhances user filtering.""" + + pass + + def _split_cell_methods(nc_cell_methods: str) -> List[re.Match]: """ Split a CF cell_methods attribute string into a list of zero or more cell @@ -256,7 +292,11 @@ def _split_cell_methods(nc_cell_methods: str) -> List[re.Match]: "Cell methods may be incorrectly parsed due to mismatched " "brackets" ) - warnings.warn(msg, UserWarning, stacklevel=2) + warnings.warn( + msg, + category=iris.exceptions.IrisCfLoadWarning, + stacklevel=2, + ) if bracket_depth > 0 and ind in name_start_inds: name_start_inds.remove(ind) @@ -275,14 +315,21 @@ def _split_cell_methods(nc_cell_methods: str) -> List[re.Match]: msg = ( f"Failed to fully parse cell method string: {nc_cell_methods}" ) - warnings.warn(msg, UserWarning, stacklevel=2) + warnings.warn( + msg, category=iris.exceptions.IrisCfLoadWarning, stacklevel=2 + ) continue nc_cell_methods_matches.append(nc_cell_method_match) return nc_cell_methods_matches -class UnknownCellMethodWarning(Warning): +class UnknownCellMethodWarning(iris.exceptions.IrisUnknownCellMethodWarning): + """ + Backwards compatible form of :class:`iris.exceptions.IrisUnknownCellMethodWarning`. + """ + + # TODO: remove at the next major release. pass @@ -320,7 +367,7 @@ def parse_cell_methods(nc_cell_methods): msg = "NetCDF variable contains unknown cell method {!r}" warnings.warn( msg.format("{}".format(method_words[0])), - UnknownCellMethodWarning, + category=UnknownCellMethodWarning, ) d[_CM_METHOD] = method name = d[_CM_NAME] @@ -389,7 +436,6 @@ def parse_cell_methods(nc_cell_methods): ################################################################################ def build_cube_metadata(engine): """Add the standard meta data to the cube.""" - cf_var = engine.cf_var cube = engine.cube @@ -436,7 +482,10 @@ def build_cube_metadata(engine): cube.attributes[str(attr_name)] = attr_value except ValueError as e: msg = "Skipping global attribute {!r}: {}" - warnings.warn(msg.format(attr_name, str(e))) + warnings.warn( + msg.format(attr_name, str(e)), + category=_WarnComboIgnoringLoad, + ) ################################################################################ @@ -479,7 +528,7 @@ def _get_ellipsoid(cf_grid_var): "applied. To apply the datum when loading, use the " "iris.FUTURE.datum_support flag." ) - warnings.warn(wmsg, FutureWarning, stacklevel=14) + warnings.warn(wmsg, category=FutureWarning, stacklevel=14) datum = None if datum is not None: @@ -512,7 +561,10 @@ def build_rotated_coordinate_system(engine, cf_grid_var): 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("Rotated pole position is not fully specified") + warnings.warn( + "Rotated pole position is not fully specified", + category=iris.exceptions.IrisCfLoadWarning, + ) north_pole_grid_lon = getattr( cf_grid_var, CF_ATTR_GRID_NORTH_POLE_GRID_LON, 0.0 @@ -859,7 +911,10 @@ def get_attr_units(cf_var, attributes): msg = "Ignoring netCDF variable {!r} invalid units {!r}".format( cf_var.cf_name, attr_units ) - warnings.warn(msg) + warnings.warn( + msg, + category=_WarnComboIgnoringCfLoad, + ) attributes["invalid_units"] = attr_units attr_units = UNKNOWN_UNIT_STRING @@ -948,7 +1003,8 @@ def get_cf_bounds_var(cf_coord_var): if attr_bounds is not None and attr_climatology is not None: warnings.warn( "Ignoring climatology in favour of bounds attribute " - "on NetCDF variable {!r}.".format(cf_coord_var.cf_name) + "on NetCDF variable {!r}.".format(cf_coord_var.cf_name), + category=_WarnComboIgnoringCfLoad, ) return cf_bounds_var, climatological @@ -1007,7 +1063,10 @@ 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(msg.format(str(cf_coord_var.cf_name))) + warnings.warn( + msg.format(str(cf_coord_var.cf_name)), + category=_WarnComboDefaultingLoad, + ) # Get any coordinate bounds. cf_bounds_var, climatological = get_cf_bounds_var(cf_coord_var) @@ -1017,7 +1076,10 @@ 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(msg.format(str(cf_coord_var.cf_name))) + warnings.warn( + msg.format(str(cf_coord_var.cf_name)), + category=_WarnComboDefaultingLoad, + ) # Handle transposed bounds where the vertex dimension is not # the last one. Test based on shape to support different # dimension names. @@ -1082,7 +1144,10 @@ def build_dimension_coordinate( "Failed to create {name!r} dimension coordinate: {error}\n" "Gracefully creating {name!r} auxiliary coordinate instead." ) - warnings.warn(msg.format(name=str(cf_coord_var.cf_name), error=e_msg)) + warnings.warn( + msg.format(name=str(cf_coord_var.cf_name), error=e_msg), + category=_WarnComboDefaultingCfLoad, + ) coord = iris.coords.AuxCoord( points_data, standard_name=standard_name, @@ -1097,7 +1162,10 @@ def build_dimension_coordinate( try: cube.add_aux_coord(coord, data_dims) except iris.exceptions.CannotAddError as e_msg: - warnings.warn(coord_skipped_msg.format(error=e_msg)) + warnings.warn( + coord_skipped_msg.format(error=e_msg), + category=iris.exceptions.IrisCannotAddWarning, + ) coord_skipped = True else: # Add the dimension coordinate to the cube. @@ -1108,7 +1176,10 @@ 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(coord_skipped_msg.format(error=e_msg)) + warnings.warn( + coord_skipped_msg.format(error=e_msg), + category=iris.exceptions.IrisCannotAddWarning, + ) coord_skipped = True if not coord_skipped: @@ -1186,7 +1257,10 @@ 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(msg.format(name=str(cf_coord_var.cf_name), error=e_msg)) + warnings.warn( + msg.format(name=str(cf_coord_var.cf_name), error=e_msg), + category=iris.exceptions.IrisCannotAddWarning, + ) else: # Make a list with names, stored on the engine, so we can find them all later. engine.cube_parts["coordinates"].append((coord, cf_coord_var.cf_name)) @@ -1237,7 +1311,10 @@ 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(msg.format(name=str(cf_cm_var.cf_name), error=e_msg)) + warnings.warn( + msg.format(name=str(cf_cm_var.cf_name), error=e_msg), + category=iris.exceptions.IrisCannotAddWarning, + ) else: # Make a list with names, stored on the engine, so we can find them all later. engine.cube_parts["cell_measures"].append( @@ -1286,7 +1363,10 @@ 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(msg.format(name=str(cf_av_var.cf_name), error=e_msg)) + warnings.warn( + msg.format(name=str(cf_av_var.cf_name), error=e_msg), + category=iris.exceptions.IrisCannotAddWarning, + ) else: # Make a list with names, stored on the engine, so we can find them all later. engine.cube_parts["ancillary_variables"].append( @@ -1503,7 +1583,8 @@ def has_supported_mercator_parameters(engine, cf_name): ): warnings.warn( "It does not make sense to provide both " - '"scale_factor_at_projection_origin" and "standard_parallel".' + '"scale_factor_at_projection_origin" and "standard_parallel".', + category=iris.exceptions.IrisCfInvalidCoordParamWarning, ) is_valid = False @@ -1533,7 +1614,10 @@ def has_supported_polar_stereographic_parameters(engine, cf_name): latitude_of_projection_origin != 90 and latitude_of_projection_origin != -90 ): - warnings.warn('"latitude_of_projection_origin" must be +90 or -90.') + warnings.warn( + '"latitude_of_projection_origin" must be +90 or -90.', + category=iris.exceptions.IrisCfInvalidCoordParamWarning, + ) is_valid = False if ( @@ -1542,14 +1626,16 @@ def has_supported_polar_stereographic_parameters(engine, cf_name): ): warnings.warn( "It does not make sense to provide both " - '"scale_factor_at_projection_origin" and "standard_parallel".' + '"scale_factor_at_projection_origin" and "standard_parallel".', + category=iris.exceptions.IrisCfInvalidCoordParamWarning, ) is_valid = False if scale_factor_at_projection_origin is None and standard_parallel is None: warnings.warn( 'One of "scale_factor_at_projection_origin" and ' - '"standard_parallel" is required.' + '"standard_parallel" is required.', + category=iris.exceptions.IrisCfInvalidCoordParamWarning, ) is_valid = False diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index 2ed01846bd..f412955adb 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -23,6 +23,7 @@ import numpy as np import numpy.ma as ma +import iris.exceptions from iris.fileformats.netcdf import _thread_safe_nc import iris.util @@ -280,7 +281,10 @@ 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(message % (name, nc_var_name)) + warnings.warn( + message % (name, nc_var_name), + category=iris.exceptions.IrisCfMissingVarWarning, + ) else: result[name] = CFAncillaryDataVariable( name, variables[name] @@ -323,7 +327,10 @@ 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(message % (name, nc_var_name)) + warnings.warn( + message % (name, nc_var_name), + category=iris.exceptions.IrisCfMissingVarWarning, + ) else: # Restrict to non-string type i.e. not a CFLabelVariable. if not _is_str_dtype(variables[name]): @@ -369,7 +376,10 @@ 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(message % (name, nc_var_name)) + warnings.warn( + message % (name, nc_var_name), + category=iris.exceptions.IrisCfMissingVarWarning, + ) else: result[name] = CFBoundaryVariable( name, variables[name] @@ -441,7 +451,10 @@ 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(message % (name, nc_var_name)) + warnings.warn( + message % (name, nc_var_name), + category=iris.exceptions.IrisCfMissingVarWarning, + ) else: result[name] = CFClimatologyVariable( name, variables[name] @@ -582,7 +595,8 @@ def identify(cls, variables, ignore=None, target=None, warn=True): if warn: message = "Missing CF-netCDF formula term variable %r, referenced by netCDF variable %r" warnings.warn( - message % (variable_name, nc_var_name) + message % (variable_name, nc_var_name), + category=iris.exceptions.IrisCfMissingVarWarning, ) else: if variable_name not in result: @@ -646,7 +660,10 @@ 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(message % (name, nc_var_name)) + warnings.warn( + message % (name, nc_var_name), + category=iris.exceptions.IrisCfMissingVarWarning, + ) else: result[name] = CFGridMappingVariable( name, variables[name] @@ -685,7 +702,10 @@ 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(message % (name, nc_var_name)) + warnings.warn( + message % (name, nc_var_name), + category=iris.exceptions.IrisCfMissingVarWarning, + ) else: # Register variable, but only allow string type. var = variables[name] @@ -857,7 +877,8 @@ def identify(cls, variables, ignore=None, target=None, warn=True): if warn: message = "Missing CF-netCDF measure variable %r, referenced by netCDF variable %r" warnings.warn( - message % (variable_name, nc_var_name) + message % (variable_name, nc_var_name), + category=iris.exceptions.IrisCfMissingVarWarning, ) else: result[variable_name] = CFMeasureVariable( @@ -1069,7 +1090,8 @@ def __init__(self, file_source, warn=False, monotonic=False): ]: warnings.warn( "Optimise CF-netCDF loading by converting data from NetCDF3 " - 'to NetCDF4 file format using the "nccopy" command.' + 'to NetCDF4 file format using the "nccopy" command.', + category=iris.exceptions.IrisLoadWarning, ) self._check_monotonic = monotonic @@ -1210,7 +1232,10 @@ def _build(cf_variable): cf_variable.dimensions, ) ) - warnings.warn(msg) + warnings.warn( + msg, + category=iris.exceptions.IrisCfNonSpanningVarWarning, + ) # Build CF data variable relationships. if isinstance(cf_variable, CFDataVariable): @@ -1261,7 +1286,10 @@ def _build(cf_variable): cf_variable.dimensions, ) ) - warnings.warn(msg) + warnings.warn( + msg, + category=iris.exceptions.IrisCfNonSpanningVarWarning, + ) # Add the CF group to the variable. cf_variable.cf_group = cf_group diff --git a/lib/iris/fileformats/name_loaders.py b/lib/iris/fileformats/name_loaders.py index 0189a8806f..cb8867b6ea 100644 --- a/lib/iris/fileformats/name_loaders.py +++ b/lib/iris/fileformats/name_loaders.py @@ -17,7 +17,7 @@ import iris.coord_systems from iris.coords import AuxCoord, CellMethod, DimCoord import iris.cube -from iris.exceptions import TranslationError +from iris.exceptions import IrisLoadWarning, TranslationError import iris.util EARTH_RADIUS = 6371229.0 @@ -273,7 +273,9 @@ def _parse_units(units): try: units = cf_units.Unit(units) except ValueError: - warnings.warn("Unknown units: {!r}".format(units)) + warnings.warn( + "Unknown units: {!r}".format(units), category=IrisLoadWarning + ) units = cf_units.Unit(None) return units @@ -611,7 +613,9 @@ 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)) + warnings.warn( + 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 20d255ea44..29202af89e 100644 --- a/lib/iris/fileformats/netcdf/loader.py +++ b/lib/iris/fileformats/netcdf/loader.py @@ -50,6 +50,15 @@ NetCDFDataProxy = _thread_safe_nc.NetCDFDataProxy +class _WarnComboIgnoringBoundsLoad( + iris.exceptions.IrisIgnoringBoundsWarning, + iris.exceptions.IrisLoadWarning, +): + """One-off combination of warning classes - enhances user filtering.""" + + pass + + def _actions_engine(): # Return an 'actions engine', which provides a pyke-rules-like interface to # the core cf translation code. @@ -352,7 +361,8 @@ def coord_from_term(term): return coord warnings.warn( "Unable to find coordinate for variable " - "{!r}".format(name) + "{!r}".format(name), + category=iris.exceptions.IrisFactoryCoordNotFoundWarning, ) if formula_type == "atmosphere_sigma_coordinate": @@ -393,7 +403,10 @@ def coord_from_term(term): coord_p0.name() ) ) - warnings.warn(msg) + warnings.warn( + msg, + category=_WarnComboIgnoringBoundsLoad, + ) coord_a = coord_from_term("a") if coord_a is not None: if coord_a.units.is_unknown(): @@ -584,7 +597,10 @@ def load_cubes(file_sources, callback=None, constraints=None): try: _load_aux_factory(engine, cube) except ValueError as e: - warnings.warn("{}".format(e)) + warnings.warn( + "{}".format(e), + category=iris.exceptions.IrisLoadWarning, + ) # Perform any user registered callback function. cube = run_callback(callback, cube, cf_var, file_source) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index c0cfd3d10b..1ff69df1f7 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -157,6 +157,15 @@ } +class _WarnComboMaskSave( + iris.exceptions.IrisMaskValueMatchWarning, + iris.exceptions.IrisSaveWarning, +): + """One-off combination of warning classes - enhances user filtering.""" + + pass + + class CFNameCoordMap: """Provide a simple CF name to CF coordinate mapping.""" @@ -308,7 +317,12 @@ def _data_fillvalue_check(arraylib, data, check_value): return is_masked, contains_value -class SaverFillValueWarning(UserWarning): +class SaverFillValueWarning(iris.exceptions.IrisSaverFillValueWarning): + """ + Backwards compatible form of :class:`iris.exceptions.IrisSaverFillValueWarning`. + """ + + # TODO: remove at the next major release. pass @@ -359,7 +373,10 @@ def _fillvalue_report(fill_info, is_masked, contains_fill_value, warn=False): ) if warn and result is not None: - warnings.warn(result) + warnings.warn( + result, + category=_WarnComboMaskSave, + ) return result @@ -733,7 +750,7 @@ def write( msg = "cf_profile is available but no {} defined.".format( "cf_patch" ) - warnings.warn(msg) + warnings.warn(msg, category=iris.exceptions.IrisCfSaveWarning) @staticmethod def check_attribute_compliance(container, data_dtype): @@ -1144,7 +1161,7 @@ def _add_aux_factories(self, cube, cf_var_cube, dimension_names): "Unable to determine formula terms " "for AuxFactory: {!r}".format(factory) ) - warnings.warn(msg) + warnings.warn(msg, category=iris.exceptions.IrisSaveWarning) else: # Override `standard_name`, `long_name`, and `axis` of the # primary coord that signals the presence of a dimensionless @@ -2126,7 +2143,10 @@ def add_ellipsoid(ellipsoid): # osgb (a specific tmerc) elif isinstance(cs, iris.coord_systems.OSGB): - warnings.warn("OSGB coordinate system not yet handled") + warnings.warn( + "OSGB coordinate system not yet handled", + category=iris.exceptions.IrisSaveWarning, + ) # lambert azimuthal equal area elif isinstance( @@ -2195,7 +2215,8 @@ def add_ellipsoid(ellipsoid): warnings.warn( "Unable to represent the horizontal " "coordinate system. The coordinate system " - "type %r is not yet implemented." % type(cs) + "type %r is not yet implemented." % type(cs), + category=iris.exceptions.IrisSaveWarning, ) self._coord_systems.append(cs) @@ -2359,7 +2380,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) + warnings.warn(msg, category=iris.exceptions.IrisCfSaveWarning) _setncattr(cf_var, attr_name, value) @@ -2593,7 +2614,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) + warnings.warn( + delayed_warning, category=iris.exceptions.IrisSaveWarning + ) return result_warnings @@ -2911,7 +2934,7 @@ def is_valid_packspec(p): msg = "cf_profile is available but no {} defined.".format( "cf_patch_conventions" ) - warnings.warn(msg) + warnings.warn(msg, category=iris.exceptions.IrisCfSaveWarning) # Add conventions attribute. sman.update_global_attributes(Conventions=conventions) diff --git a/lib/iris/fileformats/nimrod_load_rules.py b/lib/iris/fileformats/nimrod_load_rules.py index fd1ccb0e95..17db0644ee 100644 --- a/lib/iris/fileformats/nimrod_load_rules.py +++ b/lib/iris/fileformats/nimrod_load_rules.py @@ -16,7 +16,11 @@ import iris import iris.coord_systems from iris.coords import DimCoord -from iris.exceptions import CoordinateNotFoundError, TranslationError +from iris.exceptions import ( + CoordinateNotFoundError, + IrisNimrodTranslationWarning, + TranslationError, +) __all__ = ["run"] @@ -28,7 +32,12 @@ ) -class TranslationWarning(Warning): +class TranslationWarning(IrisNimrodTranslationWarning): + """ + Backwards compatible form of :class:`iris.exceptions.IrisNimrodTranslationWarning`. + """ + + # TODO: remove at the next major release. pass @@ -181,7 +190,8 @@ def units(cube, field): warnings.warn( "Unhandled units '{0}' recorded in cube attributes.".format( field_units - ) + ), + category=IrisNimrodTranslationWarning, ) cube.attributes["invalid_units"] = field_units @@ -417,7 +427,8 @@ def coord_system(field, handle_metadata_errors): if any([is_missing(field, v) for v in crs_args]): warnings.warn( "Coordinate Reference System is not completely defined. " - "Plotting and reprojection may be impaired." + "Plotting and reprojection may be impaired.", + category=IrisNimrodTranslationWarning, ) coord_sys = iris.coord_systems.TransverseMercator( *crs_args, iris.coord_systems.GeogCS(**ellipsoid) @@ -539,7 +550,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) + warnings.warn(msg, category=IrisNimrodTranslationWarning) coord_point = field.vertical_coord if coord_point == 8888.0: @@ -586,7 +597,7 @@ def vertical_coord(cube, field): warnings.warn( "Vertical coord {!r} not yet handled" "".format(field.vertical_coord_type), - TranslationWarning, + category=TranslationWarning, ) @@ -831,7 +842,8 @@ def probability_coord(cube, field, handle_metadata_errors): ) warnings.warn( f"No default units for {coord_name} coord of {cube.name()}. " - "Meta-data may be incomplete." + "Meta-data may be incomplete.", + category=IrisNimrodTranslationWarning, ) new_coord = iris.coords.AuxCoord( np.array(coord_val, dtype=np.float32), bounds=bounds, **coord_keys diff --git a/lib/iris/fileformats/pp.py b/lib/iris/fileformats/pp.py index 65e0e16d72..e19ba3adff 100644 --- a/lib/iris/fileformats/pp.py +++ b/lib/iris/fileformats/pp.py @@ -27,6 +27,7 @@ from iris._lazy_data import as_concrete_data, as_lazy_data, is_lazy_data import iris.config import iris.coord_systems +import iris.exceptions # NOTE: this is for backwards-compatitibility *ONLY* # We could simply remove it for v2.0 ? @@ -220,6 +221,33 @@ } +class _WarnComboLoadingMask( + iris.exceptions.IrisLoadWarning, + iris.exceptions.IrisMaskValueMatchWarning, +): + """One-off combination of warning classes - enhances user filtering.""" + + pass + + +class _WarnComboLoadingDefaulting( + iris.exceptions.IrisDefaultingWarning, + iris.exceptions.IrisLoadWarning, +): + """One-off combination of warning classes - enhances user filtering.""" + + pass + + +class _WarnComboIgnoringLoad( + iris.exceptions.IrisIgnoringWarning, + iris.exceptions.IrisLoadWarning, +): + """One-off combination of warning classes - enhances user filtering.""" + + pass + + class STASH(collections.namedtuple("STASH", "model section item")): """ A class to hold a single STASH code. @@ -1165,7 +1193,10 @@ 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(msg.format(mdi)) + warnings.warn( + msg.format(mdi), + category=_WarnComboLoadingMask, + ) if isinstance(data, ma.MaskedArray): if ma.is_masked(data): data = data.filled(fill_value=mdi) @@ -1290,7 +1321,8 @@ def save(self, file_handle): warnings.warn( "Downcasting array precision from float64 to float32" " for save.If float64 precision is required then" - " please save in a different format" + " please save in a different format", + category=_WarnComboLoadingDefaulting, ) data = data.astype(">f4") lb[self.HEADER_DICT["lbuser"][0]] = 1 @@ -1732,7 +1764,8 @@ def _interpret_fields(fields): warnings.warn( "Landmask compressed fields existed without a " "landmask to decompress with. The data will have " - "a shape of (0, 0) and will not read." + "a shape of (0, 0) and will not read.", + category=iris.exceptions.IrisLoadWarning, ) mask_shape = (0, 0) else: @@ -1901,7 +1934,10 @@ 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(msg) + warnings.warn( + msg, + category=_WarnComboIgnoringLoad, + ) break # Skip the trailing 4-byte word containing the header length @@ -1921,7 +1957,8 @@ def _field_gen(filename, read_data_bytes, little_ended=False): warnings.warn( wmsg.format( pp_field.lblrec * PP_WORD_DEPTH, len_of_data_plus_extra - ) + ), + category=_WarnComboIgnoringLoad, ) break diff --git a/lib/iris/fileformats/pp_save_rules.py b/lib/iris/fileformats/pp_save_rules.py index 0369fc9fd0..998255ff2b 100644 --- a/lib/iris/fileformats/pp_save_rules.py +++ b/lib/iris/fileformats/pp_save_rules.py @@ -10,6 +10,7 @@ import iris from iris.aux_factory import HybridHeightFactory, HybridPressureFactory +from iris.exceptions import IrisPpClimModifiedWarning from iris.fileformats._ff_cross_references import STASH_TRANS from iris.fileformats._pp_lbproc_pairs import LBPROC_MAP from iris.fileformats.rules import ( @@ -890,4 +891,4 @@ def verify(cube, field): def _conditional_warning(condition, warning): if condition: - warnings.warn(warning) + warnings.warn(warning, category=IrisPpClimModifiedWarning) diff --git a/lib/iris/fileformats/rules.py b/lib/iris/fileformats/rules.py index 707fd58757..d5a4b9c823 100644 --- a/lib/iris/fileformats/rules.py +++ b/lib/iris/fileformats/rules.py @@ -47,7 +47,8 @@ def as_cube(self): src_cubes = src_cubes.merge(unique=False) if len(src_cubes) > 1: warnings.warn( - "Multiple reference cubes for {}".format(self.name) + "Multiple reference cubes for {}".format(self.name), + category=iris.exceptions.IrisUserWarning, ) src_cube = src_cubes[-1] @@ -329,7 +330,7 @@ def _make_cube(field, converter): cube.units = metadata.units except ValueError: msg = "Ignoring PP invalid units {!r}".format(metadata.units) - warnings.warn(msg) + warnings.warn(msg, category=iris.exceptions.IrisIgnoringWarning) cube.attributes["invalid_units"] = metadata.units cube.units = cf_units._UNKNOWN_UNIT_STRING @@ -350,7 +351,10 @@ 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(msg.format(factory=factory_name)) + warnings.warn( + msg.format(factory=factory_name), + category=iris.exceptions.IrisUserWarning, + ) else: aux_factory = factory.factory_class(*args) cube.add_aux_factory(aux_factory) diff --git a/lib/iris/iterate.py b/lib/iris/iterate.py index cf16c9cbe6..cc82433e85 100644 --- a/lib/iris/iterate.py +++ b/lib/iris/iterate.py @@ -14,6 +14,8 @@ import numpy as np +from iris.exceptions import IrisUserWarning + __all__ = ["izip"] @@ -164,7 +166,8 @@ def izip(*cubes, **kwargs): warnings.warn( "Iterating over coordinate '%s' in step whose " "definitions match but whose values " - "differ." % coord_a.name() + "differ." % coord_a.name(), + category=IrisUserWarning, ) return _ZipSlicesIterator( diff --git a/lib/iris/pandas.py b/lib/iris/pandas.py index 4c06530627..cb26b638e4 100644 --- a/lib/iris/pandas.py +++ b/lib/iris/pandas.py @@ -29,6 +29,7 @@ from iris._deprecation import warn_deprecated from iris.coords import AncillaryVariable, AuxCoord, CellMeasure, DimCoord from iris.cube import Cube, CubeList +from iris.exceptions import IrisIgnoringWarning def _get_dimensional_metadata(name, values, calendar=None, dm_class=None): @@ -446,7 +447,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) + warnings.warn(message, category=IrisIgnoringWarning) class_arg_mapping = [] non_data_names = [] @@ -896,7 +897,7 @@ def merge_metadata(meta_var_list): "'iris.FUTURE.pandas_ndim = True'. More info is in the " "documentation." ) - warnings.warn(message, FutureWarning) + warnings.warn(message, category=FutureWarning) # The legacy behaviour. data = cube.data diff --git a/lib/iris/plot.py b/lib/iris/plot.py index ebcb5c3bcb..28b458f715 100644 --- a/lib/iris/plot.py +++ b/lib/iris/plot.py @@ -34,7 +34,7 @@ import iris.coord_systems import iris.coords import iris.cube -from iris.exceptions import IrisError +from iris.exceptions import IrisError, IrisUnsupportedPlottingWarning # Importing iris.palette to register the brewer palettes. import iris.palette @@ -2023,7 +2023,7 @@ def update_animation_iris(i, cubes, vmin, vmax, coords): "use: {}." ) msg = msg.format(plot_func.__module__, supported) - warnings.warn(msg, UserWarning) + warnings.warn(msg, category=IrisUnsupportedPlottingWarning) supported = ["contour", "contourf", "pcolor", "pcolormesh"] if plot_func.__name__ not in supported: @@ -2032,7 +2032,7 @@ def update_animation_iris(i, cubes, vmin, vmax, coords): "use: {}." ) msg = msg.format(plot_func.__name__, supported) - warnings.warn(msg, UserWarning) + warnings.warn(msg, category=IrisUnsupportedPlottingWarning) # Determine plot range. vmin = kwargs.pop("vmin", min([cc.data.min() for cc in cubes])) diff --git a/lib/iris/tests/graphics/idiff.py b/lib/iris/tests/graphics/idiff.py index 62e72f4e0e..4af7f4726d 100755 --- a/lib/iris/tests/graphics/idiff.py +++ b/lib/iris/tests/graphics/idiff.py @@ -28,6 +28,7 @@ from matplotlib.testing.exceptions import ImageComparisonFailure # noqa import matplotlib.widgets as mwidget # noqa +from iris.exceptions import IrisIgnoringWarning # noqa import iris.tests # noqa import iris.tests.graphics as graphics # noqa @@ -151,7 +152,7 @@ def step_over_diffs(result_dir, display=True): distance = graphics.get_phash(reference_image_path) - phash except FileNotFoundError: wmsg = "Ignoring unregistered test result {!r}." - warnings.warn(wmsg.format(test_key)) + warnings.warn(wmsg.format(test_key), category=IrisIgnoringWarning) continue processed = True diff --git a/lib/iris/tests/integration/experimental/test_ugrid_load.py b/lib/iris/tests/integration/experimental/test_ugrid_load.py index b0b60ee506..d94e85d2f5 100644 --- a/lib/iris/tests/integration/experimental/test_ugrid_load.py +++ b/lib/iris/tests/integration/experimental/test_ugrid_load.py @@ -19,6 +19,7 @@ import pytest from iris import Constraint, load +from iris.exceptions import IrisCfWarning from iris.experimental.ugrid.load import ( PARSE_UGRID_ON_LOAD, load_mesh, @@ -170,7 +171,7 @@ def create_synthetic_file(self, **create_kwargs): def test_mesh_bad_topology_dimension(self): # Check that the load generates a suitable warning. warn_regex = r"topology_dimension.* ignoring" - with pytest.warns(UserWarning, match=warn_regex): + with pytest.warns(IrisCfWarning, match=warn_regex): template = "minimal_bad_topology_dim" dim_line = "mesh_var:topology_dimension = 1 ;" # which is wrong ! cube = self.create_synthetic_test_cube( @@ -183,7 +184,7 @@ def test_mesh_bad_topology_dimension(self): def test_mesh_no_topology_dimension(self): # Check that the load generates a suitable warning. warn_regex = r"Mesh variable.* has no 'topology_dimension'" - with pytest.warns(UserWarning, match=warn_regex): + with pytest.warns(IrisCfWarning, match=warn_regex): template = "minimal_bad_topology_dim" dim_line = "" # don't create ANY topology_dimension property cube = self.create_synthetic_test_cube( @@ -196,7 +197,7 @@ def test_mesh_no_topology_dimension(self): def test_mesh_bad_cf_role(self): # Check that the load generates a suitable warning. warn_regex = r"inappropriate cf_role" - with pytest.warns(UserWarning, match=warn_regex): + with pytest.warns(IrisCfWarning, match=warn_regex): template = "minimal_bad_mesh_cf_role" dim_line = 'mesh_var:cf_role = "foo" ;' _ = self.create_synthetic_test_cube( @@ -206,7 +207,7 @@ def test_mesh_bad_cf_role(self): def test_mesh_no_cf_role(self): # Check that the load generates a suitable warning. warn_regex = r"no cf_role attribute" - with pytest.warns(UserWarning, match=warn_regex): + with pytest.warns(IrisCfWarning, match=warn_regex): template = "minimal_bad_mesh_cf_role" dim_line = "" _ = self.create_synthetic_test_cube( diff --git a/lib/iris/tests/integration/netcdf/test_delayed_save.py b/lib/iris/tests/integration/netcdf/test_delayed_save.py index 616feb3b0e..09f6235aab 100644 --- a/lib/iris/tests/integration/netcdf/test_delayed_save.py +++ b/lib/iris/tests/integration/netcdf/test_delayed_save.py @@ -17,8 +17,8 @@ import pytest import iris +from iris.exceptions import IrisSaverFillValueWarning from iris.fileformats.netcdf._thread_safe_nc import default_fillvals -from iris.fileformats.netcdf.saver import SaverFillValueWarning import iris.tests from iris.tests.stock import realistic_4d @@ -311,7 +311,7 @@ def test_fill_warnings(self, warning_type, output_path, save_is_delayed): result_warnings = [ log.message for log in logged_warnings - if isinstance(log.message, SaverFillValueWarning) + if isinstance(log.message, IrisSaverFillValueWarning) ] if save_is_delayed: @@ -320,7 +320,9 @@ def test_fill_warnings(self, warning_type, output_path, save_is_delayed): # Complete the operation now with warnings.catch_warnings(): # NOTE: warnings should *not* be issued here, instead they are returned. - warnings.simplefilter("error", category=SaverFillValueWarning) + warnings.simplefilter( + "error", category=IrisSaverFillValueWarning + ) result_warnings = result.compute() # Either way, we should now have 2 similar warnings. diff --git a/lib/iris/tests/integration/netcdf/test_general.py b/lib/iris/tests/integration/netcdf/test_general.py index dc0c29455f..6214f09e7e 100644 --- a/lib/iris/tests/integration/netcdf/test_general.py +++ b/lib/iris/tests/integration/netcdf/test_general.py @@ -25,7 +25,7 @@ from iris.coords import CellMethod from iris.cube import Cube, CubeList import iris.exceptions -from iris.fileformats.netcdf import Saver, UnknownCellMethodWarning +from iris.fileformats.netcdf import Saver # Get the netCDF4 module, but in a sneaky way that avoids triggering the "do not import # netCDF4" check in "iris.tests.test_coding_standards.test_netcdf4_import()". @@ -141,7 +141,9 @@ def test_unknown_method(self): warning_messages = [ warn for warn in warning_messages - if isinstance(warn, UnknownCellMethodWarning) + if isinstance( + warn, iris.exceptions.IrisUnknownCellMethodWarning + ) ] self.assertEqual(len(warning_messages), 1) message = warning_messages[0].args[0] diff --git a/lib/iris/tests/integration/netcdf/test_self_referencing.py b/lib/iris/tests/integration/netcdf/test_self_referencing.py index 3395296e11..554fabb4fc 100644 --- a/lib/iris/tests/integration/netcdf/test_self_referencing.py +++ b/lib/iris/tests/integration/netcdf/test_self_referencing.py @@ -16,6 +16,7 @@ import numpy as np import iris +from iris.exceptions import IrisCfMissingVarWarning from iris.fileformats.netcdf import _thread_safe_nc @@ -46,7 +47,9 @@ def test_cmip6_volcello_load_issue_3367(self): with mock.patch("warnings.warn") as warn: # ensure file loads without failure cube = iris.load_cube(self.fname) - warn.assert_has_calls([mock.call(expected_msg)]) + warn.assert_has_calls( + [mock.call(expected_msg, category=IrisCfMissingVarWarning)] + ) # extra check to ensure correct variable was found assert cube.standard_name == "ocean_volume" @@ -113,7 +116,9 @@ def test_self_referencing_load_issue_3367(self): with mock.patch("warnings.warn") as warn: # ensure file loads without failure cube = iris.load_cube(self.temp_dir_path) - warn.assert_called_with(expected_msg) + warn.assert_called_with( + expected_msg, category=IrisCfMissingVarWarning + ) # extra check to ensure correct variable was found assert cube.standard_name == "ocean_volume" diff --git a/lib/iris/tests/integration/test_pp.py b/lib/iris/tests/integration/test_pp.py index e654694aa7..026bdae58a 100644 --- a/lib/iris/tests/integration/test_pp.py +++ b/lib/iris/tests/integration/test_pp.py @@ -18,7 +18,7 @@ from iris.aux_factory import HybridHeightFactory, HybridPressureFactory from iris.coords import AuxCoord, CellMethod, DimCoord from iris.cube import Cube -from iris.exceptions import IgnoreCubeException +from iris.exceptions import IgnoreCubeException, IrisUserWarning import iris.fileformats.pp from iris.fileformats.pp import load_pairs_from_fields import iris.fileformats.pp_load_rules @@ -290,7 +290,7 @@ def test_hybrid_pressure_with_duplicate_references(self): "iris.fileformats.pp.load", new=load ) as load, mock.patch("warnings.warn") as warn: _, _, _ = iris.fileformats.pp.load_cubes("DUMMY") - warn.assert_called_with(msg) + warn.assert_called_with(msg, category=IrisUserWarning) def test_hybrid_height_with_non_standard_coords(self): # Check the save rules are using the AuxFactory to find the @@ -415,7 +415,7 @@ def test_hybrid_height_round_trip_no_reference(self): "Unable to create instance of HybridHeightFactory. " "The source data contains no field(s) for 'orography'." ) - warn.assert_called_with(msg) + warn.assert_called_with(msg, category=IrisUserWarning) # Check the data cube is set up to use hybrid height. self._test_coord( diff --git a/lib/iris/tests/test_coding_standards.py b/lib/iris/tests/test_coding_standards.py index 6cea9dc001..54309e3906 100644 --- a/lib/iris/tests/test_coding_standards.py +++ b/lib/iris/tests/test_coding_standards.py @@ -8,6 +8,7 @@ # importing anything else import iris.tests as tests # isort:skip +import ast from datetime import datetime from fnmatch import fnmatch from glob import glob @@ -133,6 +134,66 @@ def test_python_versions(): assert search in path.read_text() +def test_categorised_warnings(): + """ + To ensure that all UserWarnings raised by Iris are categorised, for ease of use. + + No obvious category? Use the parent: + :class:`iris.exceptions.IrisUserWarning`. + + Warning matches multiple categories? Create a one-off combo class. For + example: + + .. code-block:: python + + class _WarnComboCfDefaulting(IrisCfWarning, IrisDefaultingWarning): + \""" + One-off combination of warning classes - enhances user filtering. + \""" + pass + + """ + warns_without_category = [] + warns_with_user_warning = [] + tmp_list = [] + + for file_path in Path(IRIS_DIR).rglob("*.py"): + file_text = file_path.read_text() + parsed = ast.parse(source=file_text) + calls = filter(lambda node: hasattr(node, "func"), ast.walk(parsed)) + warn_calls = filter( + lambda c: getattr(c.func, "attr", None) == "warn", calls + ) + + warn_call: ast.Call + for warn_call in warn_calls: + warn_ref = f"{file_path}:{warn_call.lineno}" + tmp_list.append(warn_ref) + + category_kwargs = filter( + lambda k: k.arg == "category", warn_call.keywords + ) + category_kwarg: ast.keyword = next(category_kwargs, None) + + if category_kwarg is None: + warns_without_category.append(warn_ref) + # Work with Attribute or Name instances. + elif ( + getattr(category_kwarg.value, "attr", None) + or getattr(category_kwarg.value, "id", None) + ) == "UserWarning": + warns_with_user_warning.append(warn_ref) + + # This avoids UserWarnings being raised by unwritten default behaviour. + assert ( + warns_without_category == [] + ), "All warnings raised by Iris must be raised with the category kwarg." + + assert ( + warns_with_user_warning == [] + ), "No warnings raised by Iris can be the base UserWarning class." + + class TestLicenseHeaders(tests.IrisTest): @staticmethod def whatchanged_parse(whatchanged_output): diff --git a/lib/iris/tests/test_concatenate.py b/lib/iris/tests/test_concatenate.py index 9287a79fda..ec92838466 100644 --- a/lib/iris/tests/test_concatenate.py +++ b/lib/iris/tests/test_concatenate.py @@ -20,6 +20,7 @@ from iris.aux_factory import HybridHeightFactory from iris.coords import AncillaryVariable, AuxCoord, CellMeasure, DimCoord import iris.cube +from iris.exceptions import IrisUserWarning import iris.tests.stock as stock @@ -340,7 +341,8 @@ def test_points_overlap_increasing(self): cubes.append(_make_cube((0, 2), y, 1)) cubes.append(_make_cube((1, 3), y, 2)) with pytest.warns( - UserWarning, match="Found cubes with overlap on concatenate axis" + IrisUserWarning, + match="Found cubes with overlap on concatenate axis", ): result = concatenate(cubes) self.assertEqual(len(result), 2) @@ -351,7 +353,8 @@ def test_points_overlap_decreasing(self): cubes.append(_make_cube(x, (3, 0, -1), 1)) cubes.append(_make_cube(x, (1, -1, -1), 2)) with pytest.warns( - UserWarning, match="Found cubes with overlap on concatenate axis" + IrisUserWarning, + match="Found cubes with overlap on concatenate axis", ): result = concatenate(cubes) self.assertEqual(len(result), 2) @@ -366,7 +369,8 @@ def test_bounds_overlap_increasing(self): ) cubes.append(cube) with pytest.warns( - UserWarning, match="Found cubes with overlap on concatenate axis" + IrisUserWarning, + match="Found cubes with overlap on concatenate axis", ): result = concatenate(cubes) self.assertEqual(len(result), 2) @@ -381,7 +385,8 @@ def test_bounds_overlap_decreasing(self): ) cubes.append(cube) with pytest.warns( - UserWarning, match="Found cubes with overlap on concatenate axis" + IrisUserWarning, + match="Found cubes with overlap on concatenate axis", ): result = concatenate(cubes) self.assertEqual(len(result), 2) diff --git a/lib/iris/tests/test_coordsystem.py b/lib/iris/tests/test_coordsystem.py index 7cd15297cc..2e5aef249c 100644 --- a/lib/iris/tests/test_coordsystem.py +++ b/lib/iris/tests/test_coordsystem.py @@ -18,6 +18,7 @@ ) import iris.coords import iris.cube +from iris.exceptions import IrisUserWarning import iris.tests.stock @@ -341,7 +342,7 @@ def test_inverse_flattening_change(self): cs = GeogCS(6543210, 6500000) initial_crs = cs.as_cartopy_crs() with self.assertWarnsRegex( - UserWarning, + IrisUserWarning, "Setting inverse_flattening does not affect other properties of the GeogCS object.", ): cs.inverse_flattening = cs.inverse_flattening + 1 diff --git a/lib/iris/tests/test_hybrid.py b/lib/iris/tests/test_hybrid.py index 76fc971a08..b070f36a7a 100644 --- a/lib/iris/tests/test_hybrid.py +++ b/lib/iris/tests/test_hybrid.py @@ -18,6 +18,7 @@ import iris from iris.aux_factory import HybridHeightFactory, HybridPressureFactory +from iris.exceptions import IrisIgnoringBoundsWarning import iris.tests.stock @@ -136,7 +137,7 @@ def test_invalid_dependencies(self): with warnings.catch_warnings(): # Cause all warnings to raise Exceptions warnings.simplefilter("error") - with self.assertRaises(UserWarning): + with self.assertRaises(IrisIgnoringBoundsWarning): _ = HybridHeightFactory(orography=sigma) def test_bounded_orography(self): @@ -154,7 +155,7 @@ def test_bounded_orography(self): with warnings.catch_warnings(): # Cause all warnings to raise Exceptions warnings.simplefilter("error") - with self.assertRaisesRegex(UserWarning, msg): + with self.assertRaisesRegex(IrisIgnoringBoundsWarning, msg): self.cube.coord("altitude") @@ -215,7 +216,7 @@ def test_invalid_dependencies(self): with warnings.catch_warnings(): # Cause all warnings to raise Exceptions warnings.simplefilter("error") - with self.assertRaises(UserWarning): + with self.assertRaises(IrisIgnoringBoundsWarning): _ = HybridPressureFactory( sigma=sigma, surface_air_pressure=sigma ) @@ -235,7 +236,7 @@ def test_bounded_surface_pressure(self): with warnings.catch_warnings(): # Cause all warnings to raise Exceptions warnings.simplefilter("error") - with self.assertRaisesRegex(UserWarning, msg): + with self.assertRaisesRegex(IrisIgnoringBoundsWarning, msg): self.cube.coord("air_pressure") diff --git a/lib/iris/tests/test_iterate.py b/lib/iris/tests/test_iterate.py index ec86d2f69d..6317ef32b5 100644 --- a/lib/iris/tests/test_iterate.py +++ b/lib/iris/tests/test_iterate.py @@ -22,6 +22,7 @@ import iris import iris.analysis +from iris.exceptions import IrisUserWarning import iris.iterate import iris.tests.stock @@ -365,12 +366,12 @@ def test_izip_different_valued_coords(self): warnings.simplefilter( "error" ) # Cause all warnings to raise Exceptions - with self.assertRaises(UserWarning): + with self.assertRaises(IrisUserWarning): iris.iterate.izip( self.cube_a, self.cube_b, coords=self.coord_names ) # Call with coordinates, rather than names - with self.assertRaises(UserWarning): + with self.assertRaises(IrisUserWarning): iris.iterate.izip( self.cube_a, self.cube_b, coords=[latitude, longitude] ) diff --git a/lib/iris/tests/test_netcdf.py b/lib/iris/tests/test_netcdf.py index 6438140ed9..2e389942bf 100644 --- a/lib/iris/tests/test_netcdf.py +++ b/lib/iris/tests/test_netcdf.py @@ -26,6 +26,7 @@ from iris._lazy_data import is_lazy_data import iris.analysis.trajectory import iris.coord_systems as icoord_systems +from iris.exceptions import IrisCfSaveWarning from iris.fileformats._nc_load_rules import helpers as ncload_helpers import iris.fileformats.netcdf from iris.fileformats.netcdf import _thread_safe_nc @@ -1099,7 +1100,9 @@ def test_conflicting_global_attributes(self): with self.temp_filename(suffix=".nc") as filename: with mock.patch("warnings.warn") as warn: iris.save([self.cube, self.cube2], filename) - warn.assert_called_with(expected_msg) + warn.assert_called_with( + expected_msg, category=IrisCfSaveWarning + ) self.assertCDL( filename, ("netcdf", "netcdf_save_confl_global_attr.cdl") ) diff --git a/lib/iris/tests/unit/analysis/cartography/test_project.py b/lib/iris/tests/unit/analysis/cartography/test_project.py index 8649cc55ea..c00830aacc 100644 --- a/lib/iris/tests/unit/analysis/cartography/test_project.py +++ b/lib/iris/tests/unit/analysis/cartography/test_project.py @@ -16,6 +16,7 @@ import iris.coord_systems import iris.coords import iris.cube +from iris.exceptions import IrisDefaultingWarning import iris.tests import iris.tests.stock @@ -161,7 +162,8 @@ def test_no_coord_system(self): warn.assert_called_once_with( "Coordinate system of latitude and " "longitude coordinates is not specified. " - "Assuming WGS84 Geodetic." + "Assuming WGS84 Geodetic.", + category=IrisDefaultingWarning, ) diff --git a/lib/iris/tests/unit/analysis/geometry/test_geometry_area_weights.py b/lib/iris/tests/unit/analysis/geometry/test_geometry_area_weights.py index 49e03a1174..62ab1ae283 100644 --- a/lib/iris/tests/unit/analysis/geometry/test_geometry_area_weights.py +++ b/lib/iris/tests/unit/analysis/geometry/test_geometry_area_weights.py @@ -21,6 +21,7 @@ from iris.analysis.geometry import geometry_area_weights from iris.coords import DimCoord from iris.cube import Cube +from iris.exceptions import IrisGeometryExceedWarning import iris.tests.stock as stock @@ -148,7 +149,9 @@ def test_distinct_xy_bounds_pole(self): "The geometry exceeds the " "cube's y dimension at the upper end.", ) - self.assertTrue(issubclass(w[-1].category, UserWarning)) + self.assertTrue( + issubclass(w[-1].category, IrisGeometryExceedWarning) + ) target = np.array( [ [0, top_cell_half, top_cell_half, 0], diff --git a/lib/iris/tests/unit/coords/test_Coord.py b/lib/iris/tests/unit/coords/test_Coord.py index 69b6b70c96..c548d017f2 100644 --- a/lib/iris/tests/unit/coords/test_Coord.py +++ b/lib/iris/tests/unit/coords/test_Coord.py @@ -19,7 +19,7 @@ import iris from iris.coords import AuxCoord, Coord, DimCoord from iris.cube import Cube -from iris.exceptions import UnitConversionError +from iris.exceptions import IrisVagueMetadataWarning, UnitConversionError from iris.tests.unit.coords import CoordTestMixin Pair = collections.namedtuple("Pair", "points bounds") @@ -482,7 +482,7 @@ def test_numeric_nd_multidim_bounds_warning(self): "Collapsing a multi-dimensional coordinate. " "Metadata may not be fully descriptive for 'y'." ) - with self.assertWarnsRegex(UserWarning, msg): + with self.assertWarnsRegex(IrisVagueMetadataWarning, msg): coord.collapsed() def test_lazy_nd_multidim_bounds_warning(self): @@ -493,7 +493,7 @@ def test_lazy_nd_multidim_bounds_warning(self): "Collapsing a multi-dimensional coordinate. " "Metadata may not be fully descriptive for 'y'." ) - with self.assertWarnsRegex(UserWarning, msg): + with self.assertWarnsRegex(IrisVagueMetadataWarning, msg): coord.collapsed() def test_numeric_nd_noncontiguous_bounds_warning(self): @@ -504,7 +504,7 @@ def test_numeric_nd_noncontiguous_bounds_warning(self): "Collapsing a non-contiguous coordinate. " "Metadata may not be fully descriptive for 'y'." ) - with self.assertWarnsRegex(UserWarning, msg): + with self.assertWarnsRegex(IrisVagueMetadataWarning, msg): coord.collapsed() def test_lazy_nd_noncontiguous_bounds_warning(self): @@ -515,7 +515,7 @@ def test_lazy_nd_noncontiguous_bounds_warning(self): "Collapsing a non-contiguous coordinate. " "Metadata may not be fully descriptive for 'y'." ) - with self.assertWarnsRegex(UserWarning, msg): + with self.assertWarnsRegex(IrisVagueMetadataWarning, msg): coord.collapsed() def test_numeric_3_bounds(self): @@ -530,7 +530,7 @@ def test_numeric_3_bounds(self): r"1D coordinates with 2 bounds. Metadata may not be fully " r"descriptive for 'x'. Ignoring bounds." ) - with self.assertWarnsRegex(UserWarning, msg): + with self.assertWarnsRegex(IrisVagueMetadataWarning, msg): collapsed_coord = coord.collapsed() self.assertFalse(collapsed_coord.has_lazy_points()) @@ -553,7 +553,7 @@ def test_lazy_3_bounds(self): r"1D coordinates with 2 bounds. Metadata may not be fully " r"descriptive for 'x'. Ignoring bounds." ) - with self.assertWarnsRegex(UserWarning, msg): + with self.assertWarnsRegex(IrisVagueMetadataWarning, msg): collapsed_coord = coord.collapsed() self.assertTrue(collapsed_coord.has_lazy_points()) diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index a733665df8..443c9db546 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -40,6 +40,8 @@ AncillaryVariableNotFoundError, CellMeasureNotFoundError, CoordinateNotFoundError, + IrisUserWarning, + IrisVagueMetadataWarning, UnitConversionError, ) import iris.tests.stock as stock @@ -676,7 +678,10 @@ def _assert_warn_collapse_without_weight(self, coords, warn): # Ensure that warning is raised. msg = "Collapsing spatial coordinate {!r} without weighting" for coord in coords: - self.assertIn(mock.call(msg.format(coord)), warn.call_args_list) + self.assertIn( + mock.call(msg.format(coord), category=IrisUserWarning), + warn.call_args_list, + ) def _assert_nowarn_collapse_without_weight(self, coords, warn): # Ensure that warning is not raised. @@ -765,7 +770,10 @@ def _assert_warn_cannot_check_contiguity(self, warn): f"bounds. Metadata may not be fully descriptive for " f"'{coord}'. Ignoring bounds." ) - self.assertIn(mock.call(msg), warn.call_args_list) + self.assertIn( + mock.call(msg, category=IrisVagueMetadataWarning), + warn.call_args_list, + ) def _assert_cube_as_expected(self, cube): """Ensure that cube data and coordinates are as expected.""" diff --git a/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridAuxiliaryCoordinateVariable.py b/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridAuxiliaryCoordinateVariable.py index a4e0e05a08..641b6b7b44 100644 --- a/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridAuxiliaryCoordinateVariable.py +++ b/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridAuxiliaryCoordinateVariable.py @@ -20,6 +20,7 @@ import numpy as np import pytest +import iris.exceptions from iris.experimental.ugrid.cf import CFUGridAuxiliaryCoordinateVariable from iris.tests.unit.experimental.ugrid.cf.test_CFUGridReader import ( netcdf_ugrid_variable, @@ -215,7 +216,10 @@ def test_warn(self): } def operation(warn: bool): - warnings.warn("emit at least 1 warning") + warnings.warn( + "emit at least 1 warning", + category=iris.exceptions.IrisUserWarning, + ) result = CFUGridAuxiliaryCoordinateVariable.identify( vars_all, warn=warn ) @@ -223,7 +227,9 @@ def operation(warn: bool): # Missing warning. warn_regex = rf"Missing CF-netCDF auxiliary coordinate variable {subject_name}.*" - with pytest.warns(UserWarning, match=warn_regex): + with pytest.warns( + iris.exceptions.IrisCfMissingVarWarning, match=warn_regex + ): operation(warn=True) with pytest.warns() as record: operation(warn=False) @@ -235,7 +241,9 @@ def operation(warn: bool): vars_all[subject_name] = netcdf_ugrid_variable( subject_name, "", np.bytes_ ) - with pytest.warns(UserWarning, match=warn_regex): + with pytest.warns( + iris.exceptions.IrisCfLabelVarWarning, match=warn_regex + ): operation(warn=True) with pytest.warns() as record: operation(warn=False) diff --git a/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridConnectivityVariable.py b/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridConnectivityVariable.py index 27d5c1db90..5a68a8c03f 100644 --- a/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridConnectivityVariable.py +++ b/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridConnectivityVariable.py @@ -20,6 +20,7 @@ import numpy as np import pytest +import iris.exceptions from iris.experimental.ugrid.cf import CFUGridConnectivityVariable from iris.experimental.ugrid.mesh import Connectivity from iris.tests.unit.experimental.ugrid.cf.test_CFUGridReader import ( @@ -204,7 +205,10 @@ def test_warn(self): } def operation(warn: bool): - warnings.warn("emit at least 1 warning") + warnings.warn( + "emit at least 1 warning", + category=iris.exceptions.IrisUserWarning, + ) result = CFUGridConnectivityVariable.identify(vars_all, warn=warn) self.assertDictEqual({}, result) @@ -212,7 +216,9 @@ def operation(warn: bool): warn_regex = ( rf"Missing CF-UGRID connectivity variable {subject_name}.*" ) - with pytest.warns(UserWarning, match=warn_regex): + with pytest.warns( + iris.exceptions.IrisCfMissingVarWarning, match=warn_regex + ): operation(warn=True) with pytest.warns() as record: operation(warn=False) @@ -224,7 +230,9 @@ def operation(warn: bool): vars_all[subject_name] = netcdf_ugrid_variable( subject_name, "", np.bytes_ ) - with pytest.warns(UserWarning, match=warn_regex): + with pytest.warns( + iris.exceptions.IrisCfLabelVarWarning, match=warn_regex + ): operation(warn=True) with pytest.warns() as record: operation(warn=False) diff --git a/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridMeshVariable.py b/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridMeshVariable.py index 6b278cf1b1..8302c30177 100644 --- a/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridMeshVariable.py +++ b/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridMeshVariable.py @@ -20,6 +20,7 @@ import numpy as np import pytest +import iris.exceptions from iris.experimental.ugrid.cf import CFUGridMeshVariable from iris.tests.unit.experimental.ugrid.cf.test_CFUGridReader import ( netcdf_ugrid_variable, @@ -247,13 +248,18 @@ def test_warn(self): } def operation(warn: bool): - warnings.warn("emit at least 1 warning") + warnings.warn( + "emit at least 1 warning", + category=iris.exceptions.IrisUserWarning, + ) result = CFUGridMeshVariable.identify(vars_all, warn=warn) self.assertDictEqual({}, result) # Missing warning. warn_regex = rf"Missing CF-UGRID mesh variable {subject_name}.*" - with pytest.warns(UserWarning, match=warn_regex): + with pytest.warns( + iris.exceptions.IrisCfMissingVarWarning, match=warn_regex + ): operation(warn=True) with pytest.warns() as record: operation(warn=False) @@ -265,7 +271,9 @@ def operation(warn: bool): vars_all[subject_name] = netcdf_ugrid_variable( subject_name, "", np.bytes_ ) - with pytest.warns(UserWarning, match=warn_regex): + with pytest.warns( + iris.exceptions.IrisCfLabelVarWarning, match=warn_regex + ): operation(warn=True) with pytest.warns() as record: operation(warn=False) diff --git a/lib/iris/tests/unit/fileformats/ff/test_FF2PP.py b/lib/iris/tests/unit/fileformats/ff/test_FF2PP.py index cec4f53bc3..16943c0c15 100644 --- a/lib/iris/tests/unit/fileformats/ff/test_FF2PP.py +++ b/lib/iris/tests/unit/fileformats/ff/test_FF2PP.py @@ -15,7 +15,7 @@ import numpy as np -from iris.exceptions import NotYetImplementedError +from iris.exceptions import IrisLoadWarning, NotYetImplementedError import iris.fileformats._ff as ff from iris.fileformats._ff import FF2PP import iris.fileformats.pp as pp @@ -467,7 +467,7 @@ def test_unequal_spacing_eitherside(self): with mock.patch("warnings.warn") as warn: result = ff2pp._det_border(field_x, None) - warn.assert_called_with(msg) + warn.assert_called_with(msg, category=IrisLoadWarning) self.assertIs(result, field_x) def test_increasing_field_values(self): diff --git a/lib/iris/tests/unit/fileformats/ff/test_FFHeader.py b/lib/iris/tests/unit/fileformats/ff/test_FFHeader.py index 6a65397086..72d522ec85 100644 --- a/lib/iris/tests/unit/fileformats/ff/test_FFHeader.py +++ b/lib/iris/tests/unit/fileformats/ff/test_FFHeader.py @@ -14,7 +14,7 @@ import numpy as np -from iris.fileformats._ff import FFHeader +from iris.fileformats._ff import FFHeader, _WarnComboLoadingDefaulting MyGrid = collections.namedtuple("MyGrid", "column row real horiz_grid_type") @@ -60,7 +60,8 @@ def test_unknown(self): grid = header.grid() warn.assert_called_with( "Staggered grid type: 0 not currently" - " interpreted, assuming standard C-grid" + " interpreted, assuming standard C-grid", + category=_WarnComboLoadingDefaulting, ) self.assertIs(grid, mock.sentinel.grid) diff --git a/lib/iris/tests/unit/fileformats/name_loaders/test__build_cell_methods.py b/lib/iris/tests/unit/fileformats/name_loaders/test__build_cell_methods.py index ded635984c..624837c19d 100644 --- a/lib/iris/tests/unit/fileformats/name_loaders/test__build_cell_methods.py +++ b/lib/iris/tests/unit/fileformats/name_loaders/test__build_cell_methods.py @@ -15,6 +15,7 @@ from unittest import mock import iris.coords +from iris.exceptions import IrisLoadWarning from iris.fileformats.name_loaders import _build_cell_methods @@ -104,7 +105,7 @@ def test_unrecognised(self): "Unknown {} statistic: {!r}. Unable to " "create cell method.".format(coord_name, unrecognised_heading) ) - warn.assert_called_with(expected_msg) + warn.assert_called_with(expected_msg, category=IrisLoadWarning) def test_unrecognised_similar_to_no_averaging(self): unrecognised_headings = [ @@ -129,7 +130,7 @@ def test_unrecognised_similar_to_no_averaging(self): "Unknown {} statistic: {!r}. Unable to " "create cell method.".format(coord_name, unrecognised_heading) ) - warn.assert_called_with(expected_msg) + warn.assert_called_with(expected_msg, category=IrisLoadWarning) if __name__ == "__main__": diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/__init__.py b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/__init__.py index 399a987f11..38882810d2 100644 --- a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/__init__.py +++ b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/__init__.py @@ -12,6 +12,7 @@ import tempfile import warnings +from iris.exceptions import IrisLoadWarning import iris.fileformats._nc_load_rules.engine from iris.fileformats.cf import CFReader import iris.fileformats.netcdf @@ -138,7 +139,7 @@ def run_testcase(self, warning_regex=None, **testcase_kwargs): if warning_regex is None: context = self.assertNoWarningsRegexp() else: - context = self.assertWarnsRegex(UserWarning, warning_regex) + context = self.assertWarnsRegex(IrisLoadWarning, warning_regex) with context: cube = self.load_cube_from_cdl(cdl_string, cdl_path, nc_path) diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_parse_cell_methods.py b/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_parse_cell_methods.py index 729a2d8b14..9935a6e5ae 100644 --- a/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_parse_cell_methods.py +++ b/lib/iris/tests/unit/fileformats/nc_load_rules/helpers/test_parse_cell_methods.py @@ -15,6 +15,7 @@ from unittest import mock from iris.coords import CellMethod +from iris.exceptions import IrisCfLoadWarning from iris.fileformats._nc_load_rules.helpers import parse_cell_methods @@ -123,7 +124,7 @@ def test_comment_bracket_mismatch_warning(self): ] for cell_method_str in cell_method_strings: with self.assertWarns( - UserWarning, + IrisCfLoadWarning, msg="Cell methods may be incorrectly parsed due to mismatched brackets", ): _ = parse_cell_methods(cell_method_str) @@ -139,7 +140,7 @@ def test_badly_formatted_warning(self): ] for cell_method_str in cell_method_strings: with self.assertWarns( - UserWarning, + IrisCfLoadWarning, msg=f"Failed to fully parse cell method string: {cell_method_str}", ): _ = parse_cell_methods(cell_method_str) diff --git a/lib/iris/tests/unit/fileformats/netcdf/loader/test__load_aux_factory.py b/lib/iris/tests/unit/fileformats/netcdf/loader/test__load_aux_factory.py index 841935cc81..c15c8737fd 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/loader/test__load_aux_factory.py +++ b/lib/iris/tests/unit/fileformats/netcdf/loader/test__load_aux_factory.py @@ -16,6 +16,7 @@ from iris.coords import DimCoord from iris.cube import Cube +from iris.exceptions import IrisFactoryCoordNotFoundWarning from iris.fileformats.netcdf.loader import _load_aux_factory @@ -165,7 +166,8 @@ def test_formula_terms_ap_missing_coords(self): with mock.patch("warnings.warn") as warn: _load_aux_factory(self.engine, self.cube) warn.assert_called_once_with( - "Unable to find coordinate for variable " "'ap'" + "Unable to find coordinate for variable " "'ap'", + category=IrisFactoryCoordNotFoundWarning, ) self._check_no_delta() diff --git a/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py b/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py index 12af318c01..af0d7bcd30 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py +++ b/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py @@ -31,6 +31,7 @@ ) from iris.coords import AuxCoord, DimCoord from iris.cube import Cube +from iris.exceptions import IrisMaskValueMatchWarning from iris.fileformats.netcdf import Saver, _thread_safe_nc import iris.tests.stock as stock @@ -555,7 +556,7 @@ def test_contains_fill_value_passed(self): cube = self._make_cube(">f4") fill_value = 1 with self.assertWarnsRegex( - UserWarning, + IrisMaskValueMatchWarning, "contains unmasked data points equal to the fill-value", ): with self._netCDF_var(cube, fill_value=fill_value): @@ -567,7 +568,7 @@ def test_contains_fill_value_byte(self): cube = self._make_cube(">i1") fill_value = 1 with self.assertWarnsRegex( - UserWarning, + IrisMaskValueMatchWarning, "contains unmasked data points equal to the fill-value", ): with self._netCDF_var(cube, fill_value=fill_value): @@ -579,7 +580,7 @@ def test_contains_default_fill_value(self): cube = self._make_cube(">f4") cube.data[0, 0] = _thread_safe_nc.default_fillvals["f4"] with self.assertWarnsRegex( - UserWarning, + IrisMaskValueMatchWarning, "contains unmasked data points equal to the fill-value", ): with self._netCDF_var(cube): diff --git a/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver__lazy_stream_data.py b/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver__lazy_stream_data.py index 9686c88abf..da5f2d88fa 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver__lazy_stream_data.py +++ b/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver__lazy_stream_data.py @@ -18,6 +18,7 @@ import numpy as np import pytest +from iris.exceptions import IrisMaskValueMatchWarning import iris.fileformats.netcdf._thread_safe_nc as threadsafe_nc from iris.fileformats.netcdf.saver import Saver, _FillvalueCheckInfo @@ -183,5 +184,5 @@ def test_warnings(self, compute, data_form): if n_expected_warnings > 0: warning = issued_warnings[0] msg = "contains unmasked data points equal to the fill-value, 2.0" - assert isinstance(warning, UserWarning) + assert isinstance(warning, IrisMaskValueMatchWarning) assert msg in warning.args[0] diff --git a/lib/iris/tests/unit/fileformats/netcdf/saver/test__fillvalue_report.py b/lib/iris/tests/unit/fileformats/netcdf/saver/test__fillvalue_report.py index b2e4b63e3a..317f75bb8c 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/saver/test__fillvalue_report.py +++ b/lib/iris/tests/unit/fileformats/netcdf/saver/test__fillvalue_report.py @@ -11,9 +11,9 @@ import numpy as np import pytest +from iris.exceptions import IrisSaverFillValueWarning from iris.fileformats.netcdf._thread_safe_nc import default_fillvals from iris.fileformats.netcdf.saver import ( - SaverFillValueWarning, _fillvalue_report, _FillvalueCheckInfo, ) @@ -93,12 +93,14 @@ def test_warn(self, has_collision): expected_msg = "'' contains unmasked data points equal to the fill-value" # Enter a warnings context that checks for the error. warning_context = pytest.warns( - SaverFillValueWarning, match=expected_msg + IrisSaverFillValueWarning, match=expected_msg ) warning_context.__enter__() else: # Check that we get NO warning of the expected type. - warnings.filterwarnings("error", category=SaverFillValueWarning) + warnings.filterwarnings( + "error", category=IrisSaverFillValueWarning + ) # Do call: it should raise AND return a warning, ONLY IF there was a collision. result = _fillvalue_report( diff --git a/lib/iris/tests/unit/fileformats/nimrod_load_rules/test_vertical_coord.py b/lib/iris/tests/unit/fileformats/nimrod_load_rules/test_vertical_coord.py index 44dcf8ac48..2279bcffc3 100644 --- a/lib/iris/tests/unit/fileformats/nimrod_load_rules/test_vertical_coord.py +++ b/lib/iris/tests/unit/fileformats/nimrod_load_rules/test_vertical_coord.py @@ -63,7 +63,7 @@ def test_unhandled(self): vertical_coord_val=1.0, vertical_coord_type=-1 ) warn.assert_called_once_with( - "Vertical coord -1 not yet handled", TranslationWarning + "Vertical coord -1 not yet handled", category=TranslationWarning ) def test_null(self): diff --git a/lib/iris/tests/unit/fileformats/pp/test_PPField.py b/lib/iris/tests/unit/fileformats/pp/test_PPField.py index 316894ded1..f2bbf97a80 100644 --- a/lib/iris/tests/unit/fileformats/pp/test_PPField.py +++ b/lib/iris/tests/unit/fileformats/pp/test_PPField.py @@ -13,6 +13,7 @@ import numpy as np +from iris.exceptions import IrisDefaultingWarning, IrisMaskValueMatchWarning import iris.fileformats.pp as pp from iris.fileformats.pp import PPField, SplittableInt @@ -91,7 +92,7 @@ def field_checksum(data): data_64 = np.linspace(0, 1, num=10, endpoint=False).reshape(2, 5) checksum_32 = field_checksum(data_64.astype(">f4")) msg = "Downcasting array precision from float64 to float32 for save." - with self.assertWarnsRegex(UserWarning, msg): + with self.assertWarnsRegex(IrisDefaultingWarning, msg): checksum_64 = field_checksum(data_64.astype(">f8")) self.assertEqual(checksum_32, checksum_64) @@ -104,7 +105,7 @@ def test_masked_mdi_value_warning(self): [1.0, field.bmdi, 3.0], dtype=np.float32 ) msg = "PPField data contains unmasked points" - with self.assertWarnsRegex(UserWarning, msg): + with self.assertWarnsRegex(IrisMaskValueMatchWarning, msg): with self.temp_filename(".pp") as temp_filename: with open(temp_filename, "wb") as pp_file: field.save(pp_file) @@ -116,7 +117,7 @@ def test_unmasked_mdi_value_warning(self): # Make float32 data, as float64 default produces an extra warning. field.data = np.array([1.0, field.bmdi, 3.0], dtype=np.float32) msg = "PPField data contains unmasked points" - with self.assertWarnsRegex(UserWarning, msg): + with self.assertWarnsRegex(IrisMaskValueMatchWarning, msg): with self.temp_filename(".pp") as temp_filename: with open(temp_filename, "wb") as pp_file: field.save(pp_file)