Conversation
Lifts the array-hashing machinery out of _concatenate.py into a new private module, lib/iris/_combine_common.py, so that _merge.py can reach it without importing _concatenate.py. The substrate imports nothing from Iris at runtime; the coordinate annotations on array_id sit behind TYPE_CHECKING. Names lose their underscore prefix on the move, the module itself already being private. The two local variables that would otherwise have shadowed the imported array_id are renamed to points_id and bounds_id. Pure relocation: no behaviour change. Row 1 of the roadmap in docs/src/developers_guide/specs/2026-09-10-merge-concatenate-design.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`iris._combine_common` sits beneath both `iris._merge` and `iris._concatenate`, so either engine must be able to import it with no risk of a circular import. That property holds today by inspection, but nothing enforces it, and the merge side has yet to be written -- by the time the invariant is load-bearing, the commit that broke it would be long merged. Parse the module and walk its AST, collecting every module imported outside an `if TYPE_CHECKING:` block, and assert that none of them are Iris. A second test guards the guard, checking that the visitor really does distinguish a runtime import from an annotation-only one, so the first test cannot pass by failing to look. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dropping the underscore from `_array_id` put the function on the same name as three local bindings inside `compute_hashes`, which previously sat alongside it without collision. Nothing in either scope calls the function, so the shadowing is harmless today, but it is precisely the trap that had to be defused on the `_concatenate.py` side of the move, and leaving it in the module that defines the name invites the next reader to fall into it. Rename the unused element of the `group_key` unpack to `_`, and the loop variable in `compute_hashes` to `key`. Behaviour is unchanged: these are the only three lines in which the relocated code differs from the original modulo the seven renames. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add the towncrier fragment, move roadmap row 1 to "in progress", and cite this pull request against the two decisions it puts to a reviewer. Row 1 is marked in progress rather than complete: the spec says the table is updated as each pull request *lands*, and marking one's own unmerged work complete in its own diff is not a claim this programme should be making. It flips on merge. Decision 6 -- whether the moved names drop their underscore prefix -- gains the one piece of evidence implementation actually produced. The unprefixed `array_id` collides with eight existing local bindings, five of which would raise `UnboundLocalError` if left alone. Every rename in this pull request beyond the seven definitions exists to clear that name's path, so declining the rename is not merely cheap, it retires all eight and leaves a pure `git mv`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## greenfield #7276 +/- ##
=============================================
Coverage ? 90.36%
=============================================
Files ? 94
Lines ? 25822
Branches ? 4796
=============================================
Hits ? 23333
Misses ? 1709
Partials ? 780 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Codex review (model: GPT-5): I reviewed the full change against The relocated implementation matches the original apart from the documented symbol and local-variable renames. Repository-wide inspection found no remaining callers of the old On the open naming question, I recommend keeping the unprefixed names. The containing Residual risk is low: an external consumer importing these private symbols directly from |
`IrisRelease.merge_back` writes a progress file for the *next* patch,
named after that patch rather than date-stamped like every other
`nothing` progress file:
next_patch_stem = self._get_file_stem().with_stem(next_patch_str)
`nothing.Progress._get_file_stem` resolves `.nothing/` against
`Path().cwd()`, which every `pytest-xdist` worker shares. Two tests
reach that code with the same version -- `TestMergeBack::test_branches`
in its `more_patches` parametrisation, and `test_next_patch_file` --
so both resolve to the one `.nothing/v1_1_1.json`.
That is a race, not a conflict. `NextPatch(...)` writes the JSON
non-atomically and then verifies by reloading it, and its logger opens
the sibling `.log` with `mode="w"`. `test_next_patch_file` loads the
same path back and asserts on its contents. With no `--dist` setting
in `pyproject.toml`, xdist schedules dynamically, so whether the two
land on the same worker varies run to run: the observed `py3.14` failure
on SciTools#7276 reproduced on no other job, and the identical commit passed on
re-run.
Give each test its own working directory. An autouse fixture is the
right scope because `_get_file_stem` runs during `__post_init__`, so
every construction of `IrisRelease` is exposed, not just the two tests
that collide today. All three `subprocess` calls in
`release_do_nothing.py` are the git helpers already mocked by autouse
fixtures, and every other path it builds is anchored to `Path(__file__)`,
so moving the working directory reaches nothing else.
This also stops the suite writing `.nothing/` into the repository as a
side effect -- the reason `**/.nothing` sits in `.gitignore`.
The race cannot be reproduced on demand, so the accompanying test
asserts the isolation that prevents it instead of the failure itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@SciTools/iris-devs Anyone remotely interested in engaging with this? If not, I'll close it. |
|
I doubt whether I'm the right person for reviewing, but this looks like a really great development, as merge is a very time consuming operation at the moment, especially when there are many cubes in a file. |
|
One potential point for improvement: in my experience, human written pull request descriptions and comments are more inviting for reviewers. E.g. numpy and other popular open source projects also have a strict rule about this in their AI policy. |
* Isolate the release-tooling tests from each other's progress files
`IrisRelease.merge_back` writes a progress file for the *next* patch,
named after that patch rather than date-stamped like every other
`nothing` progress file:
next_patch_stem = self._get_file_stem().with_stem(next_patch_str)
`nothing.Progress._get_file_stem` resolves `.nothing/` against
`Path().cwd()`, which every `pytest-xdist` worker shares. Two tests
reach that code with the same version -- `TestMergeBack::test_branches`
in its `more_patches` parametrisation, and `test_next_patch_file` --
so both resolve to the one `.nothing/v1_1_1.json`.
That is a race, not a conflict. `NextPatch(...)` writes the JSON
non-atomically and then verifies by reloading it, and its logger opens
the sibling `.log` with `mode="w"`. `test_next_patch_file` loads the
same path back and asserts on its contents. With no `--dist` setting
in `pyproject.toml`, xdist schedules dynamically, so whether the two
land on the same worker varies run to run: the observed `py3.14` failure
on #7276 reproduced on no other job, and the identical commit passed on
re-run.
Give each test its own working directory. An autouse fixture is the
right scope because `_get_file_stem` runs during `__post_init__`, so
every construction of `IrisRelease` is exposed, not just the two tests
that collide today. All three `subprocess` calls in
`release_do_nothing.py` are the git helpers already mocked by autouse
fixtures, and every other path it builds is anchored to `Path(__file__)`,
so moving the working directory reaches nothing else.
This also stops the suite writing `.nothing/` into the repository as a
side effect -- the reason `**/.nothing` sits in `.gitignore`.
The race cannot be reproduced on demand, so the accompanying test
asserts the isolation that prevents it instead of the failure itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Add changelog fragment for the progress-file race fix
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
This also basically delivers #7241 |
|
From @SciTools/peloton: We think this is a worthwhile move in the right direction, but with our current resources we don't expect to have the space to commit to review this in the next few months. We're finding that, currently, reviewing is very much the bottleneck with agentic code. I think we're developing our approach to this with the current work, but this seems to us like the kind of thing with a big start up cost since this is such a new paradigm. Hopefully we will be able to streamline the review process when we understand it better and I expect this ought to be something we'd be more comfortable to review in future. |
|
@SciTools/peloton @stephenworsley I don't understand why you are raising the bar in the standard of review applied to an agentic generated PR compared to a human. For the very first time in the history of Note that, banking a design along with a change/s not only provides a rich resource for developers but it also helps agents understand why the (proposed) implementation is how it is, the historical decisions that were taken along the way, tasks/approaches/work deemed not appropriate or out of scope et al - the benefits are huge. Anyways, message received ... I'll leave this PR and associated I'm happy to discuss this further with anyone, whenever you choose to engage and want to move this forwards (or not). |
Sorry for the misunderstanding @bjlittle. I know this is a core concern of yours so I have been paying close attention to it! This is only day 3 of the core dev team engaging with full-fat agentic dev, so we are still getting used to the style, just as we would with any new human developer. So far I haven't seen anyone treating contributions with a different standard - we're just taking longer to understand them. Maybe in future we'll actually be able to lower the bar compared to a human, but we obviously need to understand the first few PRs to know if that's realistic. And since our full attention is on #6961 - another agentic workflow - we thought it would be dishonest of us to commit to
100%. But as I said it's taking a few days to get used to it.
❤️❤️❤️ |
🤖 Agentic pull request
This change was written by Claude (Opus 5), driven by @bjlittle. Commits carry a
Co-Authored-Bytrailer. Please review it as you would any other contribution —and more sceptically, if that is your inclination.
Design Specification
See Making Iris merge and concatenate robust, efficient and configurable on the greenfield feature branch for full details.
For convenience, Readthedocs has already been configure to automatically build the
greenfieldfeature branch documentation.What this does
Moves the array-hashing machinery out of
lib/iris/_concatenate.pyand into anew private module,
lib/iris/_combine_common.py.This is row 1 of the roadmap in the merge and concatenate design spec (#7274),
and implements §5.1 of that spec. Nothing else in the programme lands until this
one is reviewed — it is deliberately the smallest possible first step, and it is
as much a test of whether the approach is worth continuing as it is a change.
The step-by-step plan this follows landed separately in #7275, and is at
docs/src/developers_guide/plans/2026-09-10-hashing-substrate.mdif you want theprovenance. You should not need it to review this: the plan is a record of how
the change was arrived at, not an argument for it.
The hashing layer was added for concatenate in #5926. Merge needs exactly the
same capability, and today cannot have it without importing from
_concatenate.py— which_concatenate.pycannot offer, because it importsiris.cube. The new module imports nothing from Iris at runtime, so eitherengine can use it with no risk of a circular import.
test_imports.pyenforcesthat by walking the module's AST, and a second test guards the guard so the
first cannot pass by failing to look.
This is a relocation, not a rewrite
No behaviour changes. The moved code is identical to the original apart from
dropping the leading underscore on seven names, plus three lines noted below.
Two checks you can run yourself:
1. The moved block is unchanged apart from the renames:
Output is two hunks, three lines — the de-shadowing described below.
2. No concatenate test was touched.
test_hashing.pymoves to the newpackage unchanged but for its import lines; no other test under
lib/iris/tests/unit/concatenate/is edited.Where to actually look
Everything above is mechanical. The only part that needed judgement is the
array_idrename, in two places:_concatenate.pyassigned to a local namedarray_id. Once the imported function has that name, the local shadows it andthe next line raises
UnboundLocalError. The locals are nowpoints_idandbounds_id, which is what they always meant.compute_hashesitself had the same collision. It isinert — no scope in that module calls the function — but it is the same trap,
sitting in the module that defines the name. They are now
_andkey.Neither ruff nor mypy catches this class of shadowing, so it is worth a human
glance.
An open question for the reviewer
Dropping the underscore prefix makes this a move and a rename, and that is a
decision I would rather you took than I did.
The case for it: these names are now a small internal API consumed by two
modules, and the underscore no longer says anything the module's own
_prefixdoes not already say.
The case against: it turns a diff you could verify by eye into one you have to
read, and it is the sole cause of all eight local renames above.
If you would rather review a pure
git mvwith the underscores kept, say soand I will drop the rename. The module's location is what the rest of the
programme depends on; the spelling of the names is not, and I have no stake in
it.
Testing
pytest -n auto lib/iris/tests/unitgives7072 passedagainst7070ongreenfield— the two additions are the new import-invariant tests. The onepre-existing failure (
test__chunk_control.py::test_netcdf_v3) and tenpre-existing errors are unchanged by this branch and unrelated to it.