Repository navigation
Reproducible API diffs, griffe-keyed cache, better notes, golden diffs of real releases - #85
Conversation
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
|
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
Non-blocking
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
|
Thanks for the review. Everything is addressed in 07704e0, except Blocking
Non-blocking
Checks: Order: agreed, this lands after #82 and #83. Once they're in, I'll merge main, combine the "20" entries, resolve the CHANGELOG, add Generated by Claude Code |
…dx7x # Conflicts: # CHANGELOG.md
…dx7x # Conflicts: # CHANGELOG.md
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.jsonwas right (23 places), and the code gave different answers on different machines. griffe sorts modules by depth only and takes the rest fromos.walk, so siblings came in directory order (on ext4, a hash with a per-filesystem seed). mcp 1.28.1 importsMcpHttpClientFactoryinclient.sseandclient.streamable_http, 2.2.0 only inclient.sse. Wherestreamable_httpcame 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._loadernow sorts submodules by depth, then name. A Protocol's__call__does count as a place: code implementsMcpHttpClientFactoryto passsse_client(httpx_client_factory=...)(documented indocs/how-it-works.md).2. Key cached diffs on griffe's version (Fixes #59).
griffe_version()(griffelibfor griffe 2,griffefor 1.x) is part of the diff cache key, andsettings.griffe_versionis recorded in results.json,scan --json(report_schema3) 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:
message = (...)and then callswarnings.warn(message), soClient(proxies=...)got no hint.warn()of a name now reads the message assigned to it just before.Client()no longer acceptsproxies; do not pass it. Useproxyormountsinstead ofproxies. [diff + library]". click 8.2'sBaseCommand/MultiCommandnow nameclick.Command/click.Group.CliRunnertookcatch_exceptions: bool = Truewhere 8.1 hadmix_stderr: bool = True, and the note said "probably renamed tocatch_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'skinds;--checkwrites nothing and exits 1 on drift.tests/test_golden_diffs.pyasserts what the changelogs document, what the source shows where they say less, and the notes an agent gets. Every fixture must matchDIFF_SCHEMAand 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.walkin both orders)test_a_diff_another_griffe_made_is_made_againtest_a_warning_message_built_before_its_warn_call_is_the_hinttest_a_parameter_the_docs_say_was_removed_is_no_renametest_only_names_written_as_code_or_stated_as_the_replacement_countEach 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 unmodifiedmaintoo; cli: exit with 141 for --help and --version on a closed unbuffered pipe #81 addresses it.ruff check src tests && ruff format --check src testsandmypyare cleanCHANGELOG.md ("Unreleased") updated. The
[diff; probable rename]row is updated in README.md, the three translations anddocs/how-it-works.md.Anything reviewers should know
DIFF_SCHEMAis 20 (after review), with an entry for what commits 1 and 3 change in a diff; all four fixtures are re-recorded at it. apidiff: follow PEP 702's @deprecated through compat modules #82 and Compiled modules without a stub are not reported as removed #83 also bump to 20: whichever lands second merges the entries. This PR lands after them, then takesnew_compiledinscripts/record_diff_fixtures.py(Compiled modules without a stub are not reported as removed #83) and re-records.test_every_diff_fixture_is_what_the_releases_on_pypi_giveis markednetwork.🤖 Generated with Claude Code
https://claude.ai/code/session_01UQ2KaxtvnxyEhxp8G3MngA
Generated by Claude Code