Repository navigation
update-shape.py: apply refuses to keep an --accept-local copy that imports a name the module it writes beside it no longer defines, before anything is written, so a kept scripts/bootstrap.py no longer passes the validators and then dies on an ImportError (#604) - #616
Conversation
…t imports a name the module it writes beside it no longer defines, before anything is written, so a kept `scripts/bootstrap.py` no longer passes the validators and then dies on an ImportError (#604) #562 (#536) asked one direction only: a copy this run WRITES that imports from a module `--accept-local` KEEPS. The reverse went unasked. A project whose committed edit had `scripts/bootstrap.py` import a name from `scripts/repo_shape.py` (row `locally-modified`) ran `apply --at <a commit that dropped the name> --accept-local scripts/bootstrap.py`, got exit 0, `pins ok` and `manifest ok`, and its next `bootstrap.py --help` died with `ImportError: cannot import name ...`. The validators caught it only where the kept copy is itself a validator, and then only by rolling back after the writes. Upstream does drop public names: cac6bb2 took `link_manifest_path` and `declared_neutral_pins` out of `repo_shape.py`. The mechanism. `_refuse_kept_copies_missing_names`, at its one call site in `cmd_apply` before `confirm`, now asks both directions and raises one refusal: - A written importer and a kept module (#536, `_kept_copies_lacking`), unchanged in what it asks. - A kept importer and a written module (#604, `_kept_copies_importing_dropped`): each kept `.py` copy (a `both` or `locally-modified` row `--accept-local` named) is read from its bytes on disk with `_names_imported`; each module it imports is resolved with `_module_beside`; where that path is a copy this run writes (an `upstream-changed` row's target bytes, or an `--add` file's), a name is refused only when the module's bytes on disk define it now and the bytes the run writes do not (`_names_dropped`). `_names_defined(path)` keeps its behaviour (an `OSError` is "cannot tell") and now reads through `_names_defined_in(data)`, split out so the written module is read from its target bytes under the same rules. The rulings. Only what THIS RUN takes away is refused: a name the bytes on disk lack too already stops the kept copy, which is the project's doing, and a module with no file yet (an `--add` file) defines nothing to take away. The code stays `update-accept-local-missing-name`, and one refusal lists every pair, sorted by module and then importer. #536's lines are unchanged; a reverse line reads ` scripts/repo_shape.py (written) no longer defines shell_line, which scripts/bootstrap.py (kept) imports from it`. The opening sentence, the validators clause and the remediation are each true of every line in either direction: the clause is said only when no importer named, written or kept, is one of `kind.validators`. The remediation names the human's two exits for each side: change the kept copy (merge the upstream's version where it lacks a name; stop taking the name where the written module dropped it), commit, and run again with the same `--accept-local`; or drop the edit, which for a `both` row is `update-conflict`'s merge again, settled on the upstream's side. It names `git show <target>:<source>` of every kept copy listed, names `--accept-local` in prose only, and prints no NEXT line. Exit 2; nothing is written. No `--help` text changes, and no README.md or AGENTS.md statement became false. What it cannot tell, it does not refuse, in either direction: a kept importer whose bytes do not parse; a star import or a plain `import M`; a module, in the bytes on disk or the bytes the run writes, that does not parse, or that has a star import or a module `__getattr__`. There the validators answer after the writes, as before. Tests, appended to `tests/test_update_shape_kept_helper.py` on #562's harness (76 tests before, 105 now). A second project and holder are cut at `kept-base`, a branch off A whose `repo_shape.py` gains two names no shipped copy imports; `dropped` takes one away, and `both-ways` also has the holder's `siblings.py` import a name its `bootstrap.py` gains. - #604's case end to end: exit 2 under the code, the line naming `scripts/repo_shape.py` (written), `scripts/bootstrap.py` (kept) and the name, the tree, refs and HEAD byte-identical, no `copied` line, no NEXT line; the premise is measured (the kept `bootstrap.py` beside the target's `repo_shape.py` stops on that ImportError at `--help`). - With `--branch`: refused before any branch exists. - A kept `validate-pins.py` importing the dropped name: refused, without the validators clause. - Controls: a name the module lacked before too, and a name the written module still defines, each apply with exit 0, and the kept copy starts after the run exactly where it started before. - A family holder keeping a `bootstrap.py` that imports the dropped name: refused. - Both directions in one run (the holder at `both-ways`): one refusal, both kinds of line, sorted; also in process, with and without a kept validator. - In process, over pairs of bytes: the reverse line and its remediation; each case ruling 3 lists, the two "not taken away" cases and an `--add` module with no file on disk, none of which refuses; and the scope (another directory, a non-`.py` kept copy, a module the run leaves alone, a module no written copy is). Mutants, each killed: the reverse check removed (8 failed; the issue's case applies with `pins ok`, `manifest ok`, `check` then says `unchanged 11`, and `scripts/bootstrap.py --help` exits 1 on the ImportError); the "defined on disk now" condition dropped (7 failed, the lacked-before control among them); the written module read from disk instead of its target bytes (8 failed, #604's end-to-end case among them). Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideThis PR hardens Sequence diagram for reverse accept-local name refusalsequenceDiagram
participant Apply as apply
participant Kept as Kept importer on disk
participant Written as Written module target bytes
participant Check as Name validation
Apply->>Check: _refuse_kept_copies_missing_names()
Check->>Kept: _imports_on_disk(root, path)
Kept-->>Check: Imported names
Check->>Written: _names_dropped(root, path, data)
Written-->>Check: Names defined on disk but absent from target bytes
Check->>Check: Compare imported names with dropped names
alt Name was dropped
Check-->>Apply: _missing_name_refusal()
Apply-->>Apply: Refuse before confirm and writes
else No dropped imported name
Check-->>Apply: Continue
end
Flow diagram for pre-write accept-local name validationflowchart TD
A["apply --accept-local"] --> B["Collect kept copies and written target bytes"]
B --> C["Check written importers against kept module names"]
C --> D["Check kept importers against names dropped by written modules"]
D --> E{"Missing imported name?"}
E -->|No| F["Continue to confirmation and writes"]
E -->|Yes| G["Raise update-accept-local-missing-name"]
G --> H["No branch creation or file writes"]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
…ines each exit settles when it holds both directions, a kept non-.py copy is tested to be passed over, and _names_defined answers a path holding a NUL as before (#604) Fix round 1 for R604's review of 90aba6a (APPROVE WITH NITS), as the coordinator ruled. F1. When one refusal held both kinds of line, its remediation said "merge the upstream's version by hand into a kept copy that lacks a name, so it defines every name above", which is false of a `(written) no longer defines` line's names: those the kept copy must stop importing, not define. And "Either A, and B, commit it" put two changes, possibly to two files, under one "it". Each exit now names the lines it settles ("so it defines every name its `(kept) lacks` line names"; "so it no longer takes any name its `(written) no longer defines` line names"), and the sentence says "commit each". A refusal that holds one kind of line gives that kind's exit alone. F2. The "a kept copy that is not .py" case sat at the root, where `repo_shape` resolves to `repo_shape.py` and not `scripts/repo_shape.py`, so the `.py` filter never decided it and R604's mutant r6 (no filter on a kept copy) survived. It is now `scripts/notes.txt`, and so is #562's forward twin (a written copy that is not `.py`). F3. `_names_defined` caught only `OSError` around the read once #604 split the parse out, so a path holding a NUL raised the `ValueError` that base had answered with None. It now catches what base caught around the read: `OSError`, `ValueError` and `MemoryError`. `apply` cannot reach either input, since a row path holding a NUL is refused first. F4. The pair the two listers return is one `Disagreement` NamedTuple (module, importer, names, side), so `_missing_name_refusal` and `_missing_name_remediation` read it by field name. No behaviour change. Tests (`tests/test_update_shape_kept_helper.py`, 105 to 108): the assertions that pinned the old words now pin each exit's new words and "commit each", end to end for both directions in one run and in process; a one-direction refusal of each kind is held to give only its own exit; the two non-`.py` cases sit under `scripts/`; and `_names_defined` is held to answer None for a missing file, a path holding a NUL and a `MemoryError` on the read. Mutants over these bytes, each against the whole file, all killed: the reverse check removed (9 failed); "defined on disk now" dropped (7); the written module read from disk (9); r6, no `.py` filter on a kept copy (1); its forward twin, no `.py` filter on a written copy (1); the old remediation words (6); `except OSError` alone in `_names_defined` (1). Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-shape.py lines, a clean merge Main at 709eec1 is #588's squash (#532), which changed update-shape.py in `accept_local_exit`, `keep_local_exits`, `cmd_check`, `commit_on_branch`, `_refuse_local_drift` and `_note_untaken_additions`, none of which this branch touches. Under ruling 27 a pull request whose file main changed after its merge base needs this refresh before it lands. The merge is clean: git auto-merged update-shape.py, and nothing needed resolving. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 1 day and 3 hours by commenting |
…n-source-unread Merges origin/main 66267f0 so the PR's CI merge ref and the fix that follows run on current main. The fourteen landings since 2bf8fd4: #597, #578, #593, #589 (#449), #586, #573 (#537), #575 (#539), #566 (#528), #571, #588, #569 (#538), #582 (#523), #616 and #576 (#441, #442). Those that touch this PR's files: - adopt-project.py: #589 (`check` recomputes each pin's digest in the clone `plan` read it from: LOCAL_CLONE_RE, _recorded_clone, _why_not_rechecked, _recomputed_pin_finding, _pin_digest_findings), #582 (what `plan`, `execute` and AGENTS.md say around a run) and #566 (a source's own `single-repository.yaml`). - scaffold-project.py and docs/cli.md: #573, #569 and #576 (the `_declaration` docstring: `--pin-source` verifies the first chain link alone, which this PR's N3 wording already says). - tests/test_adopt_pins.py: #589's thirteen cases. - tests/test_scaffold_leg_path_collisions.py: #573. This commit holds the textual resolution only. One hunk conflicted, the `from shape_materialize import (...)` list at adopt-project.py:261. It is resolved as the union of both sides' changes to the list: main's added `pin_digest_from_source` (#589), and this PR's added `DIGEST_FROM_CLONE`, `DIGEST_FROM_GH`, `pin_source_path` and `refuse_an_unread_pin_source` and its removal of `one_printable_line`, whose last caller this PR deleted and to which main added none (keeping it is pyflakes' "imported but unused"). pyflakes adopt-project.py is clean on the merge. Every other file merged without a marker, and `scripts/render-cli-reference.py --check` is ok. The merge alone does not keep #589 working: this PR's `plan` records a bare `local-clone`, which names no clone for #589's `check` to read, and `check` refuses `local-clone (<path>)` as `plan-pin-malformed`. The next commit reconciles the two. Lane: openreposhape-2 (openRepoShape-2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-4 (openRepoShape-4)
Claim: #604 (comment)
What changed and why
#562 (#536) asked one direction only: a copy this run WRITES that imports from a module
--accept-localKEEPS. Nothing asked the reverse. A project whose committed edit hadscripts/bootstrap.pyimport a name fromscripts/repo_shape.py(rowlocally-modified) ranapply --at <a commit that dropped the name> --accept-local scripts/bootstrap.py. It got exit 0,pins okandmanifest ok, and its nextbootstrap.py --helpdied withImportError: cannot import name .... The validators caught it only where the kept copy is itself a validator, and then only by rolling back after the writes. Upstream does drop public names: cac6bb2 tooklink_manifest_pathanddeclared_neutral_pinsout ofrepo_shape.py._refuse_kept_copies_missing_nameskeeps its one call site incmd_apply, beforeconfirm. It now asks both directions and raises one refusal.apply --accept-local scripts/repo_shape.pycan keep an old helper that a newly copied file imports from, andmake bootstrapthen fails with ImportError #536,_kept_copies_lacking): what it asks is unchanged.apply --accept-local: a kept.pycopy that imports a name the run's newrepo_shape.pydropped passes, andmake bootstrapthen dies on an ImportError #604,_kept_copies_importing_dropped):.pycopy is read from its bytes on disk with_names_imported. A kept copy is abothorlocally-modifiedrow that--accept-localnamed._module_beside.upstream-changedrow's target bytes, or an--addfile's), a name is refused only when the module's bytes on disk define it now and the bytes the run writes do not (_names_dropped).DisagreementNamedTuple (module, importer, names, side), so the refusal and its remediation read it by field name._names_defined_in(data)is split out of_names_defined(path), so the written module is read from its target bytes under the same rules._names_defined(path)answers exactly as before: None where the read raisesOSError, theValueErrorof a path holding a NUL, orMemoryError.Only what this run takes away is refused.
--addfile) defines nothing to take away.One refusal, both directions.
The code stays
update-accept-local-missing-name. AGENTS.md lists no update-shape refusal code, so it needs no line.The lines: update-shape.py
apply --accept-local scripts/repo_shape.pycan keep an old helper that a newly copied file imports from, andmake bootstrapthen fails with ImportError #536's lines are unchanged. Every pair gets one line, sorted by module and then importer. A reverse line reads:scripts/repo_shape.py (written) no longer defines shell_line, which scripts/bootstrap.py (kept) imports from itThe opening sentence is true of every line in either direction: "--accept-local keeps this project's own bytes of a copy that disagrees with a copy this run writes beside it: one of the two imports a name from the other that the other's bytes do not define, so the importer would stop on an ImportError the first time it runs".
The validators clause ("though the validators, which never import it, pass") is said only when no importer named, written or kept, is one of
kind.validators.The remediation gives the human's two exits, and each exit names the lines it settles:
keep the edit and change the kept copy:
(kept) lacksline names";(written) no longer definesline names".Then "commit each", and run again with the same
--accept-local. A refusal that holds one kind of line gives that kind's exit alone.or drop the edit, so the upstream's copy is taken. For a
bothrow, that isupdate-conflict's merge again, settled on the upstream's side.It names
git show <target>:<source>of every kept copy listed. For a kept importer, that is the upstream's own copy, which shows what it imports instead. It names--accept-localin prose and prints no NEXT line.Exit 2. Nothing is written, and stdout has no
copiedline.Docs:
--helptext changed, so docs/cli.md is unchanged.apply's refusals and the--accept-localexits. None of them names update-shape.pyapply --accept-local scripts/repo_shape.pycan keep an old helper that a newly copied file imports from, andmake bootstrapthen fails with ImportError #536's code, and none promises that a re-run succeeds.Tests
tests/test_update_shape_kept_helper.py, appended on #562's harness: 76 tests before, 108 now. The module docstring says it covers both directions. The reverse direction has a secondAtlasandInkRouter, cut atkept-base. That is a branch off A whoserepo_shape.pygains two names no shipped copy imports. Offkept-base:droppedtakes one of the names away again;both-waysdoes the same, and also has the holder'ssiblings.pyimport a name itsbootstrap.pygains.End to end:
update-shape.py
apply --accept-local: a kept.pycopy that imports a name the run's newrepo_shape.pydropped passes, andmake bootstrapthen dies on an ImportError #604's case:checkreadsbootstrap.pyaslocally-modifiedandrepo_shape.pyasupstream-changed. Thenapply --at dropped --yes --accept-local scripts/bootstrap.pygives:scripts/repo_shape.py(written),scripts/bootstrap.py(kept) and the name;git status --porcelainempty;copiedline and no NEXT line;update-conflict(the row is notboth).The premise is measured too: the kept
bootstrap.pybeside the target'srepo_shape.pystops on that ImportError at--help.With
--branch: refused before any branch exists.A kept validator (
scripts/validate-pins.py) importing the dropped name: refused, without the validators clause.Controls:
In each, the kept copy starts after the run exactly where it started before.
A family holder keeping a
bootstrap.pythat imports the dropped name: refused.Both directions in one run (the holder at
both-ways, itsbootstrap.pybothand accepted): one refusal, with both kinds of line, sorted, each exit's words, and "commit each".In process, over pairs of bytes:
update-conflictnamed only for abothrow;import M, or the importer's own fallback;__getattr__;__getattr__;--addmodule with no file on disk;.py. The last sits underscripts/, where the.pyfilter alone decides it, and so does update-shape.py:applyrefuses to keep an--accept-localcopy of a helper that lacks a name a copy it writes imports from it, before anything is written, so a keptscripts/repo_shape.pyno longer leavesmake bootstrapdying on an ImportError after the validators passed (#536) #562's forward twin;_names_definedanswers None for a missing file, a path holding a NUL, and aMemoryErroron the read.Run at ece91f6 (Linux, py-bench), as the brief listed, with
-rs:tests/test_update_shape_kept_helper.py, the eight othertest_update_shape*.pyfiles named,test_family_printed_quoting.py,test_repo_hygiene.py,test_cli_reference.pyandtest_agents_md_codes.py. Result: 623 passed, 0 failed, 0 skipped, in 373 s. At 90aba6a the same set gave 620 passed.Mutants at ece91f6, each run against the whole new file:
--branch, the kept validator (update-validators-redand a rollback after the writes), the holder, both-ways, and four in process--addmodule and the five bytes-on-disk cases among themtest_keeping_a_copy_that_imports_a_name_the_run_drops_is_refused) among them.pyfilter on a kept copy (R604's r6)scripts/notes.txtcase.pyfilter on a written copy (r6's forward twin)scripts/notes.txtcaseexcept OSErroralone in_names_definedWith the reverse check removed, #604's case applies with exit 0,
pins okandmanifest ok: Atlas (atlas), 3 legs.checkthen saysunchanged 11andnothing to do. Thenscripts/bootstrap.py --helpin that tree exits 1:What it cannot tell
It does not refuse, in either direction, in these cases:
import Mand an attribute;__getattr__. That covers its bytes on disk, the bytes the run writes, and update-shape.pyapply --accept-local scripts/repo_shape.pycan keep an old helper that a newly copied file imports from, andmake bootstrapthen fails with ImportError #536's kept bytes.There the validators answer after the writes, as before. They roll the run back only where a validator is the copy that cannot import.
Known limits:
globals(),setattron the module orexecreads as missing. The reverse refuses it only where the written bytes bind it that way and the bytes on disk bound it plainly. A name bound and then deleted reads as defined.apply --accept-local scripts/repo_shape.pycan keep an old helper that a newly copied file imports from, andmake bootstrapthen fails with ImportError #536's. A kept copy that is not a validator, but that a validator imports, would make the clause false. Today no shippedrepo_shape.pyimports a sibling copy, so no shipped file is in that position.Review
R604 reviewed 90aba6a and approved with nits. Fix round 1 (ece91f6) made the four changes the coordinator ruled:
.pycases moved underscripts/, and R604's surviving mutant r6 is now killed._names_definedanswers a path holding a NUL as base did.DisagreementNamedTuple for the pair.R604's three out-of-scope findings go to #458 by the coordinator:
git showcommands are not shell-quoted;apply --accept-local scripts/repo_shape.pycan keep an old helper that a newly copied file imports from, andmake bootstrapthen fails with ImportError #536's readers in the reverse direction.Shared files
update-shape.pyis also edited by lane 4's #588 (fix/posix-paste-lines), inaccept_local_exit,keep_local_exits,cmd_check,commit_on_branch,_refuse_local_driftand_note_untaken_additions. This PR touches none of them. Its first changed line is about 20 lines below_note_untaken_additions. Its one other change outside its own functions is an import line,from typing import NamedTuple.git merge-tree --write-tree, with the heads read bygit ls-remoteafter the fix round:1d809eaff92154208a9a2828e40673fb86698e889d9638a7, exit 0, no conflict. Main has not touched either file since this PR's base.8f5ce8a4e5ffa0f8b854c275da564d44cc3d1db509d457db, exit 0, no conflict. The mergedupdate-shape.pyparses and differs from this head by exactly #588's 28 insertions and 12 deletions.#588's earlier head,
c2e19022, conflicted indocs/handbook.htmlagainst main whether or not this PR was in the merge. Its merge of main (8f5ce8a4) has since cleared that.Closes #604
🤖 Generated with Claude Code
Summary by Sourcery
Prevent
apply --accept-localfrom keeping Python importers that would reference names removed by modules in the same update.New Features:
apply --accept-localvalidation to detect kept importers that depend on names removed from modules written by the update.Bug Fixes:
ImportErrorafter upstream names are removed.Enhancements:
Tests: