Skip to content

Reproducible API diffs, griffe-keyed cache, better notes, golden diffs of real releases - #85

Merged
MohammadHijjawi97 merged 8 commits into
mainfrom
claude/zen-hawking-bodx7x
Oct 9, 2026
Merged

MohammadHijjawi97 merged 8 commits into
mainfrom
claude/zen-hawking-bodx7x

Conversation

@MohammadHijjawi97

@MohammadHijjawi97 MohammadHijjawi97 commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

What this changes

Four commits, one theme: API diffs and their notes that are the same on every machine and checked against real releases.

1. Read sibling modules by name, so API diffs match on every machine. The fixture mcp-1.28.1-2.2.0.json was right (23 places), and the code gave different answers on different machines. griffe sorts modules by depth only and takes the rest from os.walk, so siblings came in directory order (on ext4, a hash with a per-filesystem seed). mcp 1.28.1 imports McpHttpClientFactory in client.sse and client.streamable_http, 2.2.0 only in client.sse. Where streamable_http came first, the factory's __call__ (timeout, auth, return) did not match across releases, so 20 places instead of 23. The openai fixture's example name was the same effect. _loader now sorts submodules by depth, then name. A Protocol's __call__ does count as a place: code implements McpHttpClientFactory to pass sse_client(httpx_client_factory=...) (documented in docs/how-it-works.md).

2. Key cached diffs on griffe's version (Fixes #59). griffe_version() (griffelib for griffe 2, griffe for 1.x) is part of the diff cache key, and settings.griffe_version is recorded in results.json, scan --json (report_schema 3) and report.md. Because the key changed, diffs cached by an earlier since-cutoff are made once more, which also retires results cached before commit 1.

3. Name the replacements warnings give, and drop renames the docs refute. Recording real pairs surfaced three wrong or weak notes:

  • httpx 0.27 builds message = (...) and then calls warnings.warn(message), so Client(proxies=...) got no hint. warn() of a name now reads the message assigned to it just before.
  • A replacement stated in quotes now counts like one in backticks, and every alternative is named ("Use a, b, or c instead" names all three). The note was "found no replacement"; it is now "Client() no longer accepts proxies; do not pass it. Use proxy or mounts instead of proxies. [diff + library]". click 8.2's BaseCommand/MultiCommand now name click.Command/click.Group.
  • click 8.2's CliRunner took catch_exceptions: bool = True where 8.1 had mix_stderr: bool = True, and the note said "probably renamed to catch_exceptions", which would change what the runner does. No rename is suggested when a .. versionadded::/.. versionchanged:: note in the new docstring says one was added or the other removed (without naming both).

4. Golden diffs of httpx 0.27.2 -> 0.28.1 and click 8.1.8 -> 8.2.0 (part of #64). Also scripts/record_diff_fixtures.py, which re-records every fixture from PyPI and keeps a fixture's kinds; --check writes nothing and exits 1 on drift. tests/test_golden_diffs.py asserts what the changelogs document, what the source shows where they say less, and the notes an agent gets. Every fixture must match DIFF_SCHEMA and stay at or under 150 KB.

How it was tested

  • Each fix has a tiny-tree regression test that fails without it and passes with it:

    • test_a_protocol_two_modules_import_is_one_place_whatever_the_listing_order (os.walk in both orders)
    • test_a_diff_another_griffe_made_is_made_again
    • test_a_warning_message_built_before_its_warn_call_is_the_hint
    • test_a_parameter_the_docs_say_was_removed_is_no_rename
    • quoted names in test_only_names_written_as_code_or_stated_as_the_replacement_count
  • Each changelog claim was checked against the upstream changelog and the release source. One nuance: httpx calls cert "deprecated", but the 0.28.1 top-level functions no longer take it.

  • python scripts/record_diff_fixtures.py --check: all four fixtures unchanged, on griffe 2.3.0 and 1.14.0 (the floor).

  • pytest -m network: 16 passed. It includes a new test that re-records every fixture from PyPI.

  • pytest -m "not network": 1093 passed. One exception: test_output.py::test_a_closed_pipe_ends_the_run_quietly[help] fails in my sandbox on unmodified main too; cli: exit with 141 for --help and --version on a closed unbuffered pipe #81 addresses it.

  • ruff check src tests && ruff format --check src tests and mypy are clean

  • CHANGELOG.md ("Unreleased") updated. The [diff; probable rename] row is updated in README.md, the three translations and docs/how-it-works.md.

Anything reviewers should know

🤖 Generated with Claude Code

https://claude.ai/code/session_01UQ2KaxtvnxyEhxp8G3MngA


Generated by Claude Code

MohammadHijjawi97 and others added 4 commits October 1, 2026 22:58
griffe sorts a package's modules by depth only and takes the rest from
os.walk, so siblings came in the file system's directory order. An object
that several sibling modules import from a private module took its public
path from that order. mcp 1.28.1 imports McpHttpClientFactory in
client.sse and client.streamable_http, 2.2.0 only in client.sse: where
streamable_http was listed first, the factory's __call__ (timeout, auth and
its return) did not match between the releases, and the httpx2 switch had
20 places instead of 23. The committed fixtures were right; they were
recorded where siblings came in name order.

_loader now gives griffe a finder that sorts submodules by depth, then
name. Both fixtures in tests/fixtures/diffs re-record byte for byte, on
griffe 1.14 and 2.3.

Add scripts/record_diff_fixtures.py, which re-records every fixture from
PyPI (keeping a fixture's "kinds" filter) and with --check only says which
would change, plus an offline test of its output shape and a network test
that every fixture is what the releases on PyPI give. The regression test
reproduces mcp's case in a tiny tree, listed in both orders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQ2KaxtvnxyEhxp8G3MngA
A diff is griffe's reading of the sources as much as since-cutoff's, and
griffe is a range dependency (griffe>=1.14). The diff cache key held
DIFF_SCHEMA, the package and its versions only, so after a griffe upgrade
that reads something differently, scans kept serving the old griffe's
diff, and results.json could not say which griffe made it.

griffe_version() reads the installed version (griffelib for griffe 2,
whose griffe distribution is a meta-package; griffe for 1.x). It is part
of the diff cache key, and results.json, scan --json and report.md record
it as settings.griffe_version (report_schema 3). Since the key changed,
diffs cached by an earlier since-cutoff are made once more, which also
retires results cached before modules were read in name order.

Fixes #59

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQ2KaxtvnxyEhxp8G3MngA
Recording real release pairs showed three wrong or weak notes:

- httpx 0.27 builds its messages first (message = (...)), then calls
  warnings.warn(message). Only a literal message was read, so
  Client(proxies=...) and Client(app=...) had no hint. A warn() of a name
  now reads the message last assigned to that name before the call.
- "Use 'proxy' or 'mounts' instead." named nothing: names in quotes were
  not taken as code, and "use X instead" allowed one name in backticks.
  Quoted names now count like backticked ones, and the note names every
  alternative the text gives: "Use `proxy` or `mounts` instead of
  `proxies`. [diff + library]". click 8.2's "Use 'Command' instead." now
  names click.Command too.
- click 8.2's CliRunner took catch_exceptions: bool = True where 8.1 took
  mix_stderr: bool = True, so the note said mix_stderr was "probably
  renamed to catch_exceptions": advice that changes what the runner does.
  A rename in place is no longer suggested when the new docstring (the
  class's, for __init__) says one was added or the other removed,
  without naming both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQ2KaxtvnxyEhxp8G3MngA
Two real pairs whose breaking changes their changelogs document, recorded
with scripts/record_diff_fixtures.py (35 KB and 29 KB).
tests/test_golden_diffs.py asserts what the changelogs say (httpx removed
proxies and app; click removed HelpOption and CliRunner's mix_stderr and
deprecated BaseCommand, MultiCommand and the parser module), what the
source shows where they say less (httpx's top-level functions no longer
take cert; click's metavar methods now take ctx), and the notes an agent
gets. Every fixture must match DIFF_SCHEMA and stay under 150 KB.

Part of #64.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQ2KaxtvnxyEhxp8G3MngA
@MohammadHijjawi97 MohammadHijjawi97 changed the title Read sibling modules by name so API diffs match on every machine Reproducible API diffs, griffe-keyed cache, better notes, golden diffs of real releases Oct 1, 2026
@MohammadHijjawi97

Copy link
Copy Markdown
Owner Author

Review notes. All four parts work and each claim reproduces (the directory-order fix gives 23 places for mcp in both listing orders; main gives 20 or 23). Two things to fix before it comes out of draft:

Blocking

  1. A test was pasted into the middle of another. test_a_diff_another_griffe_made_is_made_again (tests/test_scan_first.py:872-897) sits inside test_a_cached_diff_adds_no_progress_line_to_a_log (:853), so that test's second half (lines 892-897, the RichReporter part and assert "diffed" not in out.getvalue()) now runs at the end of the new test. Move those lines back.
  2. DIFF_SCHEMA is not bumped although commits 1 and 3 change what a diff records (dependency-switch sites, hint/still_handled_text from warn(message), library_names and whether find_renamed runs, renamed/suggestions via _documented_apart). Without a bump, results.json and the note provenance give the same schema number to two different diffs. Bump it (after apidiff: follow PEP 702's @deprecated through compat modules #82/Compiled modules without a stub are not reported as removed #83, add these changes to that schema's entry) and re-record the click and httpx fixtures.

Non-blocking

  • _QUOTED (apidiff.py:3056) changes nothing any test sees but does change replacement_candidates (used by the signatures baseline) and turns quoted option values into candidates; drop it or test it.
  • tests/test_golden_diffs.py:6-8 says a change in since-cutoff or griffe "fails here", but only the network test re-diffs.
  • The app test uses an invented httpx message; pin the real 0.27.2 note instead (it currently quotes the replacement and then says none was found).
  • _documented_apart also drops a genuine rename whose docstring says "New name values ..."; tighten it to version directives or reword the docs.
  • stated_alternatives filters or case-sensitively; "Use a, b, or c instead" matches nothing; notes.py:723 should use _listed(..., "or").
  • scripts/record_diff_fixtures.py should pass new_compiled once Compiled modules without a stub are not reported as removed #83 lands.
  • README.zh-CN.md:473 uses 固定版本 where the rest of the file uses 锁定版本.

Order: land after #82 and #83; only CHANGELOG conflicts in text, and the two new fixtures need re-recording at the then-current schema.

… notes

Blocking:
- test_a_diff_another_griffe_made_is_made_again had been pasted into the
  middle of test_a_cached_diff_adds_no_progress_line_to_a_log, so that
  test's RichReporter half ran at the end of the new one. It is back where
  it belongs.
- DIFF_SCHEMA 20: name-ordered modules (dependency-switch sites), hints
  from warn(message), quoted and alternative stated replacements
  (library_names, and whether find_renamed runs), and renames a version
  note refutes (renamed, suggestions) change what a diff records. All four
  fixtures are re-recorded; only their "schema" changes.

Non-blocking:
- Drop _QUOTED: no fixture or test changes without it (the quote-aware
  "use X instead" pattern does the work), and it turned quoted option
  values into replacement candidates for the signatures baseline.
- "Use a, b, or c instead" now gives all three (a comma may be followed
  by "or"), "or" is filtered case-insensitively, and the note lists the
  alternatives with _listed ("`a`, `b` or `c`").
- _documented_apart reads only .. versionadded:: / .. versionchanged::
  notes, so a description such as "New ``start`` values ..." no longer
  blocks a genuine rename (tested).
- The httpx app test uses httpx 0.27.2's real message, and both it and the
  golden test pin the real note (advice quoted, no replacement named).
- test_golden_diffs.py no longer claims a diff change fails there: only
  the network test diffs again.
- README.zh-CN.md uses 锁定版本 like the rest of the file; the rename row
  says "version note" in all five files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQ2KaxtvnxyEhxp8G3MngA

Copy link
Copy Markdown
Owner Author

Thanks for the review. Everything is addressed in 07704e0, except new_compiled, which has to wait for #83.

Blocking

  1. Split test: fixed. The RichReporter half (out = io.StringIO() ... assert "diffed" not in out.getvalue()) is back at the end of test_a_cached_diff_adds_no_progress_line_to_a_log. Against the base, tests/test_scan_first.py now only adds lines.
  2. DIFF_SCHEMA: bumped to 20, with an entry listing what this PR changes in a diff: dependency-switch sites, hint/still_handled_text from warn(message), library_names and whether find_renamed runs, and renamed/suggestions. All four fixtures are re-recorded; only their "schema" changed. apidiff: follow PEP 702's @deprecated through compat modules #82 and Compiled modules without a stub are not reported as removed #83 each bump to 20 with their own entry, so whichever lands second merges the three entries into one, and I'll re-record the fixtures at that schema then.

Non-blocking

  • _QUOTED: dropped. No fixture or test changes without it, because the quote-aware "use X instead" pattern already covers httpx and click, so replacement_candidates is back to what it was.
  • "Use a, b, or c instead": now gives all three (a comma may be followed by "or"). or is filtered case-insensitively ("Use x Or y instead" gives x, y), and notes.py uses _listed(..., "or"). Tests for both cases are in test_notes_provenance.py.
  • _documented_apart: now reads only .. versionadded:: / .. versionchanged:: notes. New test cases: "New catch_exceptions values are accepted." stays a rename, and so does a version note that names both parameters.
  • app message: the tiny tree now uses httpx 0.27.2's real message. test_golden_diffs.py pins the real note too: advice quoted, followed by "found no replacement".
  • test_golden_diffs.py docstring: it now says these tests catch note changes, and the network test catches diff changes.
  • README.zh-CN.md: uses 锁定版本. The probable-rename row now says "version note" in all five files.
  • new_compiled: not done yet. diff_sources doesn't take it until Compiled modules without a stub are not reported as removed #83 lands. When I bring Compiled modules without a stub are not reported as removed #83 into this branch, I'll pass it in scripts/record_diff_fixtures.py and re-record.

Checks: pytest -m "not network" 1093 passed, pytest -m network 16 passed, ruff and mypy clean.

Order: agreed, this lands after #82 and #83. Once they're in, I'll merge main, combine the "20" entries, resolve the CHANGELOG, add new_compiled and re-record.


Generated by Claude Code

@MohammadHijjawi97
MohammadHijjawi97 marked this pull request as ready for review October 9, 2026 22:42
@MohammadHijjawi97
MohammadHijjawi97 merged commit 251e64f into main Oct 9, 2026
21 checks passed
MohammadHijjawi97 added a commit that referenced this pull request Oct 10, 2026
README.fr.md keeps this branch's [diff + metadata] row and main's [diff; probable rename] row (#85); CHANGELOG.md keeps both sets of Unreleased bullets.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Key cached API diffs on griffe's version, and record it in results.json

1 participant