diff --git a/lib/iris/coords.py b/lib/iris/coords.py index da712d6e2a..62936ad45d 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -380,8 +380,8 @@ def array_summary(data, n_max, n_edge, linewidth, precision): # or "years") cannot be converted to a date using # `num2date`, so gracefully fall back to printing # values as numbers. - if not self.units.is_long_time_interval(): - # Otherwise ... replace all with strings. + try: + # Replace all with strings. if ma.is_masked(data): mask = data.mask else: @@ -391,6 +391,10 @@ def array_summary(data, n_max, n_edge, linewidth, precision): # Masked datapoints do not survive num2date. if mask is not None: data = np.ma.masked_array(data, mask) + except (ValueError, TypeError): + # Fall back to numeric values if num2date fails + # (e.g., for long time intervals like months/years) + pass if ma.is_masked(data): # Masks are not handled by np.array2string, whereas diff --git a/lib/iris/experimental/raster.py b/lib/iris/experimental/raster.py index 0b5057136c..7c387d58c9 100644 --- a/lib/iris/experimental/raster.py +++ b/lib/iris/experimental/raster.py @@ -208,3 +208,4 @@ def export_geotiff(cube, fname): x_min = np.min(x_bounds) y_max = np.max(coord_y.bounds) _gdal_write_array(x_min, x_step, y_max, y_step, coord_system, data, fname, "GTiff") + \ No newline at end of file diff --git a/lib/iris/tests/experimental/regrid/test_regrid_area_weighted_rectilinear_src_and_grid.py b/lib/iris/tests/experimental/regrid/test_regrid_area_weighted_rectilinear_src_and_grid.py index 216c3383c8..11f0242786 100644 --- a/lib/iris/tests/experimental/regrid/test_regrid_area_weighted_rectilinear_src_and_grid.py +++ b/lib/iris/tests/experimental/regrid/test_regrid_area_weighted_rectilinear_src_and_grid.py @@ -13,6 +13,12 @@ import pytest import iris.analysis._interpolation +# Suppress deprecation warnings for experimental.regrid, which is the subject +# of these tests. +pytestmark = pytest.mark.filterwarnings( + "ignore:.*experimental.regrid.*:iris._deprecation.IrisDeprecation" +) + from iris.experimental.regrid import ( regrid_area_weighted_rectilinear_src_and_grid as regrid_area_weighted, ) diff --git a/lib/iris/tests/graphics/__init__.py b/lib/iris/tests/graphics/__init__.py index 2c9fc0b345..04c25131fb 100644 --- a/lib/iris/tests/graphics/__init__.py +++ b/lib/iris/tests/graphics/__init__.py @@ -285,7 +285,7 @@ def skip_plot(fn: Callable) -> Callable: ... pass """ - skip = pytest.mark.skipIf( + skip = pytest.mark.skipif( condition=not MPL_AVAILABLE, reason="Graphics tests require the matplotlib library.", ) diff --git a/lib/iris/tests/integration/experimental/regrid/test_regrid_conservative_via_esmpy.py b/lib/iris/tests/integration/experimental/regrid/test_regrid_conservative_via_esmpy.py index e6d209c691..aa3483b10c 100644 --- a/lib/iris/tests/integration/experimental/regrid/test_regrid_conservative_via_esmpy.py +++ b/lib/iris/tests/integration/experimental/regrid/test_regrid_conservative_via_esmpy.py @@ -10,6 +10,7 @@ import numpy as np import pytest +from iris._deprecation import IrisDeprecation from iris.tests import _shared_utils # Import ESMF if installed, else fail quietly + disable all the tests. @@ -24,6 +25,13 @@ import iris import iris.analysis import iris.analysis.cartography as i_cartog + +# The following functions are deprecated but testing their behavior is the purpose +# of this module. +pytestmark = pytest.mark.filterwarnings( + "ignore:.*regrid_conservative_via_esmpy.*:iris._deprecation.IrisDeprecation" +) + from iris.experimental.regrid_conservative import regrid_conservative_via_esmpy import iris.tests.stock as istk diff --git a/lib/iris/tests/integration/experimental/test_regrid_ProjectedUnstructured.py b/lib/iris/tests/integration/experimental/test_regrid_ProjectedUnstructured.py index 85f10a0c87..5e5ddae90b 100644 --- a/lib/iris/tests/integration/experimental/test_regrid_ProjectedUnstructured.py +++ b/lib/iris/tests/integration/experimental/test_regrid_ProjectedUnstructured.py @@ -12,6 +12,12 @@ import iris import iris.aux_factory from iris.coord_systems import GeogCS +# Suppress deprecation warnings for experimental.regrid, which is the subject +# of these tests. +pytestmark = pytest.mark.filterwarnings( + "ignore:.*experimental.regrid.*:iris._deprecation.IrisDeprecation" +) + from iris.experimental.regrid import ( ProjectedUnstructuredLinear, ProjectedUnstructuredNearest, diff --git a/lib/iris/tests/unit/aux_factory/test_AuxCoordFactory.py b/lib/iris/tests/unit/aux_factory/test_AuxCoordFactory.py index faf57d0e65..78c8f5fb39 100644 --- a/lib/iris/tests/unit/aux_factory/test_AuxCoordFactory.py +++ b/lib/iris/tests/unit/aux_factory/test_AuxCoordFactory.py @@ -213,7 +213,7 @@ def chunkspecs(points: _Chunkspec, bounds: _Chunkspec) -> Tuple[_Chunks, _Chunks class Test_rechunk: - class TestAuxFact(AuxCoordFactory): + class AuxFactImpl(AuxCoordFactory): """A minimal AuxCoordFactory that enables us to test the re-chunking logic.""" def __init__(self, nx, ny, nz): @@ -267,7 +267,7 @@ def test_rechunk(self, nz, deptypes): # (10, 10, 10) = 1,000: ok # (10, 10, 100) = 10,000 --> (5, 10, 100) = 5000 --> rechunk, dividing X by 2 # (10, 10, 1000) = 100,000 --> (1, 5, 1000) --> rechunk both X and Y - aux_co = self.TestAuxFact(nx, ny, nz) + aux_co = self.AuxFactImpl(nx, ny, nz) if deptypes != "all_lazy": # Touch all dependencies to realise @@ -299,7 +299,7 @@ def test_rechunk(self, nz, deptypes): }[nz] assert result_pts_bds_chunks == expected_pts_bds_chunks - class MultiDimTestFactory(TestAuxFact): + class MultiDimTestFactory(AuxFactImpl): """An extended test factory with an added multidimensional term.""" # Use fixed test dimensions, for simplicity. diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index d91c7e81c0..e71ec1e499 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -69,7 +69,12 @@ def test_masked_no_mask(self): def test_matrix(self): # Subclasses of np.ndarray should be coerced back to np.ndarray. # (Except for np.ma.MaskedArray.) - data = np.matrix([[1, 2, 3], [4, 5, 6]]) + # Create a custom ndarray subclass to replace deprecated np.matrix. + class NdArraySubclass(np.ndarray): + pass + + data_base = np.array([[1, 2, 3], [4, 5, 6]]) + data = data_base.view(NdArraySubclass) cube = Cube(data) assert type(cube.data) is np.ndarray _shared_utils.assert_array_equal(cube.data, data) diff --git a/lib/iris/tests/unit/data_manager/test_DataManager.py b/lib/iris/tests/unit/data_manager/test_DataManager.py index 2471b00316..b52fa0527c 100644 --- a/lib/iris/tests/unit/data_manager/test_DataManager.py +++ b/lib/iris/tests/unit/data_manager/test_DataManager.py @@ -332,7 +332,7 @@ def test_with_lazy_mask_array__not_masked(self): assert dm.has_lazy_data() result = dm.data assert not dm.has_lazy_data() - assert isinstance(result, np.core.ndarray) + assert isinstance(result, np.ndarray) assert dm.dtype == self.dtype assert result.fill_value == self.fill_value _shared_utils.assert_array_equal(result, self.real_array) @@ -543,11 +543,15 @@ def test_coerce_to_ndarray(self): shape = (2, 3) size = np.prod(shape) real_array = np.arange(size).reshape(shape) - matrix = np.matrix(real_array) + # Create a custom ndarray subclass to replace deprecated np.matrix. + class NdArraySubclass(np.ndarray): + pass + + matrix = real_array.view(NdArraySubclass) dm = DataManager(real_array) dm.data = matrix - assert isinstance(dm._real_array, np.core.ndarray) - assert isinstance(dm.data, np.core.ndarray) + assert isinstance(dm._real_array, np.ndarray) + assert isinstance(dm.data, np.ndarray) _shared_utils.assert_array_equal(dm.data, real_array) def test_real_masked_constant_to_array(self): diff --git a/lib/iris/tests/unit/experimental/raster/test_export_geotiff.py b/lib/iris/tests/unit/experimental/raster/test_export_geotiff.py index e1c76f50de..1b950580e6 100644 --- a/lib/iris/tests/unit/experimental/raster/test_export_geotiff.py +++ b/lib/iris/tests/unit/experimental/raster/test_export_geotiff.py @@ -8,6 +8,12 @@ import numpy as np import pytest +# Suppress deprecation warnings for experimental.raster, which is the subject +# of these tests. +pytestmark = pytest.mark.filterwarnings( + "ignore:.*experimental.raster.*:iris._deprecation.IrisDeprecation" +) + try: from osgeo import gdal diff --git a/lib/iris/tests/unit/experimental/regrid/test_regrid_area_weighted_rectilinear_src_and_grid.py b/lib/iris/tests/unit/experimental/regrid/test_regrid_area_weighted_rectilinear_src_and_grid.py index 630781d2ef..2dfdf05d5a 100644 --- a/lib/iris/tests/unit/experimental/regrid/test_regrid_area_weighted_rectilinear_src_and_grid.py +++ b/lib/iris/tests/unit/experimental/regrid/test_regrid_area_weighted_rectilinear_src_and_grid.py @@ -14,6 +14,12 @@ from iris.coord_systems import GeogCS from iris.coords import DimCoord from iris.cube import Cube +# Suppress deprecation warnings for experimental.regrid, which is the subject +# of these tests. +pytestmark = pytest.mark.filterwarnings( + "ignore:.*experimental.regrid.*:iris._deprecation.IrisDeprecation" +) + from iris.experimental.regrid import ( regrid_area_weighted_rectilinear_src_and_grid as regrid, ) diff --git a/lib/iris/tests/unit/experimental/regrid/test_regrid_weighted_curvilinear_to_rectilinear.py b/lib/iris/tests/unit/experimental/regrid/test_regrid_weighted_curvilinear_to_rectilinear.py index bb5d5a274d..3dbb772aba 100644 --- a/lib/iris/tests/unit/experimental/regrid/test_regrid_weighted_curvilinear_to_rectilinear.py +++ b/lib/iris/tests/unit/experimental/regrid/test_regrid_weighted_curvilinear_to_rectilinear.py @@ -18,6 +18,13 @@ import iris.coords from iris.coords import AuxCoord, DimCoord import iris.cube + +# Suppress deprecation warnings for experimental.regrid, which is the subject +# of these tests. +pytestmark = pytest.mark.filterwarnings( + "ignore:.*experimental.regrid.*:iris._deprecation.IrisDeprecation" +) + from iris.experimental.regrid import ( regrid_weighted_curvilinear_to_rectilinear as regrid, ) diff --git a/lib/iris/tests/unit/experimental/ugrid/test_ParseUgridOnLoad.py b/lib/iris/tests/unit/experimental/ugrid/test_ParseUgridOnLoad.py index 62961157d8..775f563028 100644 --- a/lib/iris/tests/unit/experimental/ugrid/test_ParseUgridOnLoad.py +++ b/lib/iris/tests/unit/experimental/ugrid/test_ParseUgridOnLoad.py @@ -11,6 +11,12 @@ import pytest from iris._deprecation import IrisDeprecation +# Suppress deprecation warnings for experimental.ugrid, which is the subject +# of these tests. +pytestmark = pytest.mark.filterwarnings( + "ignore:.*experimental.ugrid.*:iris._deprecation.IrisDeprecation" +) + from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD, ParseUGridOnLoad diff --git a/lib/iris/tests/unit/fileformats/ff/test_FF2PP.py b/lib/iris/tests/unit/fileformats/ff/test_FF2PP.py index e1cd1f5912..6ac1b7816d 100644 --- a/lib/iris/tests/unit/fileformats/ff/test_FF2PP.py +++ b/lib/iris/tests/unit/fileformats/ff/test_FF2PP.py @@ -6,6 +6,7 @@ import collections import contextlib +from unittest.mock import patch import numpy as np import pytest @@ -72,7 +73,7 @@ def mock_for_extract_field(self, fields, x=None, y=None): the "make_pp_field" call. """ - with self.mocker.patch("iris.fileformats._ff.FFHeader"): + with patch("iris.fileformats._ff.FFHeader"): ff2pp = ff.FF2PP("mock") ff2pp._ff_header.lookup_table = [0, 0, len(fields)] # Fake level constants, with shape specifying just one model-level. @@ -83,13 +84,13 @@ def mock_for_extract_field(self, fields, x=None, y=None): open_func = "builtins.open" with ( - self.mocker.patch( + patch( "iris.fileformats._ff._parse_binary_stream", return_value=[0] ), - self.mocker.patch(open_func), - self.mocker.patch("struct.unpack_from", return_value=[4]), - self.mocker.patch("iris.fileformats.pp.make_pp_field", side_effect=fields), - self.mocker.patch( + patch(open_func), + patch("struct.unpack_from", return_value=[4]), + patch("iris.fileformats.pp.make_pp_field", side_effect=fields), + patch( "iris.fileformats._ff.FF2PP._payload", return_value=(0, 0) ), ): @@ -217,7 +218,7 @@ def _setup(self, mocker): field.boundary_packing = None def _test(self, mock_field, expected_depth, expected_dtype, word_depth=None): - with self.mocker.patch("iris.fileformats._ff.FFHeader", return_value=None): + with patch("iris.fileformats._ff.FFHeader", return_value=None): kwargs = {} if word_depth is not None: kwargs["word_depth"] = word_depth diff --git a/plan.md b/plan.md new file mode 100644 index 0000000000..e815212e2d --- /dev/null +++ b/plan.md @@ -0,0 +1,136 @@ +# Agentic Plan for Delivery + +## Goal +Enable pytest to fail on problem warnings (particularly `DeprecationWarning`), +making it easy to detect newly-introduced issues. Achieve this by: +1. Classifying all warnings currently in the log +2. Fixing addressable warnings in Iris source/test code +3. Asserting expected warnings inside tests (not leaking) +4. Suppressing unavoidable external warnings +5. Promoting DeprecationWarning (and related) to errors in pyproject.toml + +--- + +## Warning Inventory (from pytest_warnings.log, Jun 2026) + +### A. IrisDeprecation — Iris-owned, still used in tests (MUST FIX) +- `GraphicsTestMixin`, `IrisTest`, `GraphicsTest`, `PPTest` classes deprecated (tests/__init__.py) +- `iris.experimental.regrid` / `regrid_conservative` deprecated since v3.2 (used in test_regrid_* tests) +- `iris.experimental.raster` deprecated since v3.2 +- `iris.experimental.ugrid` deprecated → `iris.mesh` +- `iris.fileformats.abf` / `iris.fileformats.dot` deprecated +- `env_bin_path` deprecated +- `iris.fileformats.netcdf.saver` legacy attribute mode deprecated since v3.8 +- `iris.analysis.maths.intersection_of_cubes` deprecated +- `iris.coord_systems.RotatedMercator` deprecated → `ObliqueMercator` +- Various `regrid_weighted_curvilinear_to_rectilinear`, `ProjectedUnstructuredNearest/Linear` deprecated + +### B. DeprecationWarning — Iris test code (MUST FIX) +- `np.core.ndarray` → `np.ndarray` in test_DataManager.py (lines 335, 549, 550) +- Replace deprecated `np.matrix` coercion tests with a custom `np.ndarray` subclass instance (created via `.view(Subclass)`), preserving the original intent: verify ndarray-subclass inputs are coerced back to plain `np.ndarray` +- `Unit.is_long_time_interval()` path is deprecated in cf-units; align Iris with cf-units PR #279 by removing this pre-check and using `num2date` with try-except fallback in coords.py + +### C. Pytest infrastructure warnings (MUST FIX) +- `PytestUnknownMarkWarning`: `pytest.mark.skipIf` (capital I) → `pytest.mark.skipif` in tests/graphics/__init__.py:288 +- `PytestCollectionWarning`: `TestAuxFact` class with `__init__` in test_AuxCoordFactory.py:216 +- `PytestMockWarning`: mocker.patch used as context manager in test_FF2PP.py (lines 75, 86, 89, 90, 91, 92, 220) + +### D. IrisUserWarning / Iris domain warnings — intentionally triggered (ASSERT IN TESTS) +These are raised by Iris logic under test; tests should explicitly assert/capture them: +- `IrisUserWarning`: collapsing spatial coord without weighting, CRS mismatch, concatenate warnings +- `IrisLoadWarning`, `IrisSaveWarning`, `IrisCfMissingVarWarning`, `IrisCfNonSpanningVarWarning` +- `IrisCfLabelVarWarning`, `IrisGuessBoundsWarning`, `IrisVagueMetadataWarning` +- `IrisDefaultingWarning`, `IrisIgnoringBoundsWarning`, `IrisPpClimModifiedWarning` +- `IrisNimrodTranslationWarning`, `IrisGeometryExceedWarning` +- `_WarnComboDefaultingLoad`, `_WarnComboIgnoringCfLoad`, `_WarnComboLoadIgnoring` + +### E. External/third-party warnings (SUPPRESS in pyproject.toml) +- `DeprecationWarning` from `distributed/client.py` (dask large graph) +- `DeprecationWarning` from `osgeo/gdal.py` (GDAL NumPy scalar conversion) +- `DeprecationWarning` from `numpy.ma.extras` / `numpy._core` (NumPy internal changes) +- `UserWarning` from `cartopy/crs.py` (Orthographic/NearsidePerspective elliptical globes) +- `UserWarning` from `distributed/client.py` (large dask graph size warning) +- `UserWarning` from `numpy.ma.core` (masked element to nan) +- `RuntimeWarning` from `numpy.ma.core`/`numpy._core` (invalid value in cast — in numpy internals) +- `RuntimeWarning` from `matplotlib/collections.py` (invalid value in sqrt) + +### F. RuntimeWarning — Iris/test code (ASSESS) +- Divide by zero in test_divide.py/maths.py (intentional, test should assert) +- `invalid value encountered in cast` in analysis/_regrid.py:818 (may need investigation) + +--- + +## PR Split +1. PR 1 covers Phases 1-3: pytest infrastructure fixes, direct deprecation fixes in Iris/tests, and migration off deprecated Iris test-framework utilities. +2. PR 2 covers Phases 4-5: assert expected domain warnings and then tighten warning policy/filtering in pytest configuration. +3. PR 2 depends on PR 1 merging first, to avoid large noise from unresolved deprecations and to keep warning-policy changes reviewable. +4. Merge gate between PRs: warning count from PR 1 branch must be stable and attributable, so PR 2 only changes behavior by policy, not by hidden code migrations. + +## Handoff Protocol +1. Scope lock per run: before any agent changes, explicitly choose the active target (`PR 1` or `PR 2`) and confirm no cross-PR work is allowed in that run. +2. Plan re-read at session start: the agent must re-read this plan and state the active scope before editing. +3. Drift check before edits: verify branch state, current warning baseline, and plan changes since last run; if any drift exists, record a delta note first. +4. PR contract boundaries: + - `PR 1` only: Phases 1-3 (infrastructure/deprecation fixes + migration off deprecated test-framework utilities). + - `PR 2` only: Phases 4-5 (expected-warning assertions + pytest warning-policy tightening). +5. No scope bleed: out-of-scope findings are logged as deferred follow-ups, not implemented in the current PR. +6. Execution log continuity: keep a dated session log of completed items, deferred items, blockers, and decisions to bridge multi-day gaps. +7. Re-entry rule after gaps: first action is always plan reload + baseline warning check, then continue implementation. +8. Human review checklist mirrors plan phases: reviewer verifies each changed file matches the active PR scope and rejects unrelated "bonus" changes. + +## Phased Implementation Plan + +### Phase 1: Fix pytest infrastructure warnings (no functional changes, quick wins) +1. Fix `pytest.mark.skipIf` → `pytest.mark.skipif` in `lib/iris/tests/graphics/__init__.py:288` +2. Fix `TestAuxFact` class `__init__` in `lib/iris/tests/unit/aux_factory/test_AuxCoordFactory.py:216` +3. Fix mocker.patch context manager usage in `lib/iris/tests/unit/fileformats/ff/test_FF2PP.py` (lines 75, 86, 89-92, 220) + +### Phase 2: Fix DeprecationWarnings in test code +4. Replace `np.core.ndarray` with `np.ndarray` in `lib/iris/tests/unit/data_manager/test_DataManager.py` (lines 335, 549, 550) +5. Replace deprecated `np.matrix` usages in `lib/iris/tests/unit/cube/test_Cube.py:72` and `lib/iris/tests/unit/data_manager/test_DataManager.py:546` with a purpose-built `np.ndarray` subclass input (for example `arr.view(Subclass)`), then assert output type is plain `np.ndarray` to preserve coercion semantics +6. Replace deprecated `is_long_time_interval()` guard in `lib/iris/coords.py:383` with a `num2date` attempt wrapped in `try/except` so invalid long-interval units fall back to numeric formatting (matching cf-units PR #279 guidance) + +### Phase 3: Remove deprecated test-framework usage and selectively assert remaining deprecations +For each group of tests that exercise deprecated APIs: +1. Prefer code migration/removal of deprecated usage; only use `pytest.warns(IrisDeprecation)` when the deprecated API itself is the behavior under test + - `test_regrid_area_weighted_rectilinear_src_and_grid.py` (regrid deprecated) + - `test_regrid_conservative_via_esmpy.py` (regrid_conservative deprecated) + - `test_raster.py` / `test_export_geotiff.py` (raster deprecated) + - `test_regrid_ProjectedUnstructured.py` (ProjectedUnstructured deprecated) + - `test_regrid_weighted_curvilinear_to_rectilinear.py` + - `test_RotatedMercator.py` + - Tests using IrisTest/GraphicsTestMixin/PPTest/GraphicsTest/env_bin_path + - `test_intersect.py` (intersection_of_cubes) + - tests using netcdf legacy save mode + - tests using iris.experimental.ugrid + +### Phase 4: Assert expected Iris domain warnings in tests (Category D) +8. Audit tests for each Iris warning class and add `pytest.warns()` assertions where warnings are expected but not currently being tested for + - Highest priority: tests that use `IrisUserWarning`, `IrisLoadWarning`, `IrisSaveWarning` + - Secondary: vague metadata, guess bounds, ignore bounds, etc. + +### Phase 5: Configure pyproject.toml filterwarnings +9. Update `pyproject.toml` `[tool.pytest.ini_options]` to: + - Promote `DeprecationWarning` (and `PendingDeprecationWarning`) to errors: `"error::DeprecationWarning"`, `"error::PendingDeprecationWarning"` + - Add targeted ignores for unavoidable external warnings (Category E above) + - Keep `"default"` as a fallback or replace with `"error"` for Iris-specific categories + +--- + +## Key Files +- `pyproject.toml` — `[tool.pytest.ini_options].filterwarnings` +- `lib/iris/tests/graphics/__init__.py` — skipIf typo (line 288) +- `lib/iris/tests/unit/aux_factory/test_AuxCoordFactory.py` — TestAuxFact __init__ (line 216) +- `lib/iris/tests/unit/fileformats/ff/test_FF2PP.py` — PytestMockWarning (lines 75, 86, 89-92, 220) +- `lib/iris/tests/unit/data_manager/test_DataManager.py` — np.core (lines 335, 549, 550), ndarray-subclass coercion test (line 546) +- `lib/iris/tests/unit/cube/test_Cube.py` — ndarray-subclass coercion test (line 72) +- `lib/iris/coords.py` — cftime.is_long_time_interval (line 383) +- All experimental regrid/raster/ugrid test files + +## Verification +1. PR 1 verification: run `pytest lib/iris --tb=no -q` and confirm infrastructure/deprecation-warning reductions from Phases 1-3 are real and expected. +2. PR 1 verification: confirm deprecated Iris test-framework utilities (IrisTest/GraphicsTestMixin/PPTest/GraphicsTest/env_bin_path) are migrated where in scope, not merely warning-wrapped. +3. PR 2 verification: run targeted warning-sensitive suites first, then full `pytest lib/iris --tb=no -q`, and confirm Phase 4 assertions pass without leaking expected warnings. +4. PR 2 verification: confirm warning policy in pytest config causes non-zero exit for newly introduced `DeprecationWarning`/`PendingDeprecationWarning`, with only explicitly approved external suppressions. +5. Final verification: no legitimate regression failures introduced by warning-policy tightening. +