Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions lib/iris/coords.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/iris/experimental/raster.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
2 changes: 1 addition & 1 deletion lib/iris/tests/graphics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that the fix for this, as per the plan.md, was to use pytest.warns in the test.
However, the fix that has been applied is a blanked filterwarning at the module level...

from iris.experimental.regrid_conservative import regrid_conservative_via_esmpy
import iris.tests.stock as istk

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions lib/iris/tests/unit/aux_factory/test_AuxCoordFactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
7 changes: 6 additions & 1 deletion lib/iris/tests/unit/cube/test_Cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 8 additions & 4 deletions lib/iris/tests/unit/data_manager/test_DataManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
15 changes: 8 additions & 7 deletions lib/iris/tests/unit/fileformats/ff/test_FF2PP.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import collections
import contextlib
from unittest.mock import patch

import numpy as np
import pytest
Expand Down Expand Up @@ -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.
Expand All @@ -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)
),
):
Expand Down Expand Up @@ -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
Expand Down
136 changes: 136 additions & 0 deletions plan.md
Original file line number Diff line number Diff line change
@@ -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.

Loading