Repository navigation
Keep Python's evaluation order, show and/or operands, harden the hooks - #4
Merged
Merged
Conversation
Checked against pytest's coverage matrix for its rewriter, vendored as testing/test_rewrite_coverage.py from the series ending in pytest-dev/pytest#14916. It failed 20 cases; three were wrong behaviour. A name was left in place and read only when the expression around it was assembled, after later operands had run: a walrus operator, or a call rebinding it through global or nonlocal, changed what it compared as, and `assert value == f(value := 3)` passed where plain Python fails. It is now read into a temporary first whenever a later operand can run code. A method was looked up after its arguments, and its receiver read after them; the bound method now goes into a temporary when an argument can run code, only ever called, never passed to v(), so RPython has nothing to render. `and`/`or` as a value showed only its result. Its later operands run on some runs only, so their values cannot go into slots every failure passes (they would read temporaries never assigned, and cannot be preset to None: RPython keeps a temporary at one type). They are appended to a list as they are evaluated, which fail() adds to the labels and values. The same problem existed at statement level: `assert (a or b) and c` reported the or's operands on the failure after it and raised UnboundLocalError; an or nests its operands only where nothing is tested after it now. Subscripts show container and key and conditional expressions their condition, as pytest-dev/pytest#14815 and #14816 do, and a comparison or and/or compared with something is parenthesized as pytest does. The flow cases translate each new shape through the llinterpreter and C. Co-Authored-By: Claude Opus 5.5 via Claude Code <noreply@anthropic.com>
Generated asserts run compiled as they are and rewritten, and must agree on the outcome, on every side effect in order, and on the names left bound; a rewritten failure must also render. The grammar mixes what makes evaluation order observable: walrus, traced calls, a traced method lookup, globals and nonlocals rebound by calls, operands that raise, and every kind of short-circuit. Against the previous commit's parent it finds the method lookup order within seconds; it also found the statement-level or fixed there. Both are kept as fixed cases. 80000 examples over eight seeds run clean. Co-Authored-By: Claude Opus 5.5 via Claude Code <noreply@anthropic.com>
Under lazy imports (PEP 810) a finder that imports anything, even sys or fnmatch, asks itself about the module it is importing: the standalone hook failed with ImportCycleError on 3.15 (pytest-dev/pytest#14632). Standard library, pytest and cot_assert names are ruled out first, from a set taken at import, by string operations only; a wildcard or python_files no longer selects them, an explicit name or a mark still does. pytest marks cot-assert itself as a plugin, and rewriting it would need itself; it is never selected. pytest-dev/pytest#15022 recognises a rewritten module by its loader being the hook, and asks the hook _should_rewrite: the pytest hook is now the loader of what it rewrites, delegating to the standalone one, and answers that. It also rewrites pkg/__init__.py named on the command line, whose initial-path stem was __init__. Checked on 3.15.0rc1 with PYTHON_LAZY_IMPORTS=all, standalone and with pytest from pytest-dev/pytest#14844. Co-Authored-By: Claude Opus 5.5 via Claude Code <noreply@anthropic.com>
The same licence as pytest, whose rewriter this mirrors and whose coverage matrix is now vendored here. All commits so far are by the one copyright holder. Co-Authored-By: Claude Opus 5.5 via Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 via Claude Code <noreply@anthropic.com>
RonnyPfannschmidt
added a commit
that referenced
this pull request
Oct 7, 2026
Add changelog fragments for #4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Written by Claude Opus 5.5 via Claude Code for the cot.assert maintainers; I prompted it, it did the work, I read it.
This checks cot.assert against pytest's rewrite coverage matrix, vendored from the series ending in pytest-dev/pytest#14916, and against a new hypothesis fuzzer. The fuzzer runs generated asserts plain and rewritten and compares outcome, side-effect order and final bindings.
Behaviour fixes
assert value == f(value := 3)passed where plain Python fails, and names rebound throughglobal/nonlocalcompared wrong.obj.take(f(obj := None))raisedAttributeError. The bound method sits in a temporary that is only called, never passed tov().assert (a or b) and craisedUnboundLocalErrorwhenawas true andcfalse. Found by the fuzzer; it predates this PR.Explanations
and/orused as a value shows its operands, as pytest does. Operands that run on some runs only are recorded in a list, which_runtime.fail()now takes as an optional fifth argument. Translated code keeps str labels and values.and/oroperands are parenthesized as pytest does.Hooks
pytestdoes not work withPYTHON_LAZY_IMPORTS=allpytest-dev/pytest#14632) no longer cycle. A wildcard orpython_filesno longer selects the standard library; an explicit name or a mark still does._should_rewrite, ready for Warn when a collected test module was imported too early, and fix three cases where it is pytest-dev/pytest#15022. It rewritespkg/__init__.pynamed on the command line, and never cot.assert itself.testing/test_rewrite_coverage.pyis MIT-licensed pytest code. It carries pytest's licence notice and is excluded from ruff so that a resync stays a copy.Checked locally:
PYTHON_LAZY_IMPORTS=allchecked standalone and with assertion rewrite: never rewrite stdlib and pytest modules (fixes lazy imports crash) pytest-dev/pytest#14844.pre-commit run -a: clean.