Skip to content

Allow int for max_warnings in TOML (#14953) - #1

Open
Dmao233 wants to merge 13 commits into
mainfrom
cursor/max-warnings-toml-int-6b43
Open

Dmao233 wants to merge 13 commits into
mainfrom
cursor/max-warnings-toml-int-6b43

Conversation

@Dmao233

@Dmao233 Dmao233 commented Aug 31, 2026 •

Copy link
Copy Markdown
Owner

The max_warnings option was registered without a type, so it defaulted to a string with an empty default. Native TOML integers such as max_warnings = 0 were then rejected (expects a string, got int), even though the configuration reference and the warnings how-to document an unquoted integer and declare the option as int.

This follows the truncation_limit_* convention from pytest-dev#14692 / pytest-dev#14675: register the option as int | str so native TOML integers and existing string values (quoted TOML, INI, -o) both remain supported. _get_max_warnings() now treats None as unset, so an explicit integer 0 is distinct from the empty default.

Fixes pytest-dev#14953

Verification

python3 -m pytest testing/test_warnings.py::TestMaxWarnings -q --tb=short
# 18 passed

Manual cases (one warning, --disable-warnings): unquoted and quoted 0 in pytest.toml and [tool.pytest], pytest.ini, -o max_warnings=0, and --max-warnings overriding ini all exit 6 (MAX_WARNINGS_ERROR). Unset still exits 0.

Checklist

  • Include new tests or update existing tests when applicable.
  • Changelog entry: changelog/14953.bugfix.rst
  • AI assistance is credited in a Co-authored-by commit trailer.
Open in Web Open in Cursor 

@Dmao233
Dmao233 marked this pull request as ready for review August 31, 2026 01:24
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 31, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-08-31T01:28:18.760634Z 3abf799 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3abf79995d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/_pytest/terminal.py Outdated
Comment on lines +1096 to +1098
if ini_value is None:
return None
return int(ini_value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat an empty string as unset

When an existing INI file contains max_warnings = or native TOML contains max_warnings = "", this new None-only check passes the empty string to int(). Previously, the truthiness check treated these values as unset; now a successful test run ends with an uncaught ValueError traceback and exit code 1. Preserve the empty-string case as unset while still distinguishing the integer 0 from the new None default.

Useful? React with 👍 / 👎.

dependabot Bot and others added 12 commits August 31, 2026 07:37
pytest-dev#14955)

Bumps [pytest-rerunfailures](https://github.com/pytest-dev/pytest-rerunfailures) from 16.5 to 16.6.
- [Changelog](https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst)
- [Commits](pytest-dev/pytest-rerunfailures@16.5...16.6)

---
updated-dependencies:
- dependency-name: pytest-rerunfailures
  dependency-version: '16.6'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.16.4 → v0.16.5](astral-sh/ruff-pre-commit@v0.16.4...v0.16.5)
- [github.com/woodruffw/zizmor-pre-commit: v1.29.0 → v1.30.0](zizmorcore/zizmor-pre-commit@v1.29.0...v1.30.0)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Fixes pytest-dev#14445 - assertion rewriting evaluated NamedExpr (:=) expressions
multiple times, causing side effects to fire repeatedly.

The root cause was the `variables_overwrite` mechanism which stored and
re-evaluated NamedExpr AST nodes in subsequent assertions, in
`_call_reprcompare`'s results tuple, and in explanation formatting.

The fix:
- visit_NamedExpr: reference the target variable in explanations instead
  of re-evaluating the full expression
- visit_Compare: assign left-side NamedExpr to a temp before right-side
  hoisting; freeze left_res when a comparator walrus targets the same
  name; replace NamedExpr entries in `results` with target variables
- visit_BoolOp: capture short-circuit condition in a stable temp for the
  explanation path; remove walrus target rename logic
- visit_Call: remove variables_overwrite substitution (walrus now properly
  assigns to user variables in its natural evaluation position)
- Remove variables_overwrite, scope tracking, Sentinel class

Co-authored-by: Claude Opus 5 <ai@anthropic.com>
Co-authored-by: Claude Code <ai@anthropic.com>
Co-authored-by: Claude Sonnet 4 <ai@anthropic.com>
Co-authored-by: Cursor <ai@cursor.com>
Add tests for two remaining walrus double-evaluation scenarios:
- Bare NamedExpr as BoolOp operand evaluated twice via condition check
- Same walrus target in chained comparison evaluated multiple times

Co-authored-by: Claude Sonnet 4 <ai@anthropic.com>
Co-authored-by: Cursor <ai@cursor.com>
Use the already-assigned res_var to build the short-circuit condition
instead of the raw visitor result, preventing bare NamedExpr operands
from being evaluated a second time when checking truthiness.

Co-authored-by: Claude Sonnet 4 <ai@anthropic.com>
Co-authored-by: Cursor <ai@cursor.com>
In a chained comparison like `(x := f()) < (x := g()) < (x := h())`,
each NamedExpr comparator is now assigned to a temp variable so it
evaluates exactly once. Previously the raw NamedExpr node would be
reused as left_res in the next iteration, causing double evaluation.

Co-authored-by: Claude Sonnet 4 <ai@anthropic.com>
Co-authored-by: Cursor <ai@cursor.com>
When multiple walrus operators target the same variable in a BoolOp
(e.g., `assert (x := side_effect()) and (x := False)`), the assertion
explanation previously showed the final value of `x` for all operands
because the format context evaluated lazily after all operands ran.

Fix by tracking Name/NamedExpr operand values in stable @py_assert
variables (via self.assign) immediately after evaluation, then pointing
the explanation format context at the tracked copy. This uses the same
value-tracking mechanism already used by visit_Call, visit_Attribute, etc.

Fixes the case reported by @bluetech in PR review.

Co-authored-by: Claude Sonnet 4 <ai@anthropic.com>
Co-authored-by: Cursor <ai@cursor.com>
Replace the blanket snapshot-all-operands approach with a targeted one:
pre-scan the BoolOp to find walrus targets, then only snapshot operands
whose value a later walrus would corrupt.

Snapshot rules:
- NamedExpr (non-last): always, to avoid re-evaluating side effects
- Name with later walrus conflict: to freeze the pre-overwrite value
- Everything else: use res directly (stable @py_assert or plain name)

Non-walrus BoolOps now generate identical code to 8.3.5 (no snapshots).

Co-authored-by: Claude Opus 4 <ai@anthropic.com>
Co-authored-by: Cursor <ai@cursor.com>
The rewriter hoists each operand into its own statement, but a plain
name is left as a bare load evaluated when the enclosing expression is
assembled -- after the statements of the operands that follow it.  A
walrus operator in a later operand rebinds the name in between, so both
the value used and the value reported were the post-walrus one, while
Python evaluates the earlier operand first:

    assert value != identity(value := value.lower())

visit_BoolOp already guarded against this; extract its pre-scan as
_walrus_targets() and add visit_operand() to apply the same freeze in
visit_Compare, visit_Call and visit_BinOp.  visit_Compare previously
matched only a comparator that *was* a NamedExpr, missing walrus
operators nested inside it; visit_Call did not guard at all, so an
earlier argument saw a later argument's assignment.

These cases predate the walrus rework -- they fail on main too.

Closes the single-eval-walrus, order-compare-left, order-call-argument
and order-binop-left groups in the coverage matrix.  order-call-argument
keeps one entry: a bare walrus argument is still substituted into a
later one, which visit_operand does not yet see because the operand is a
NamedExpr rather than a Name.

Reported-by: Denis Scapin

Co-authored-by: Claude Opus 5 <ai@anthropic.com>
Co-authored-by: Claude Code <ai@anthropic.com>
TestAssertionRewriteWalrusOperator predates the coverage matrix and asks
its questions through runpytest(): twelve tests that mostly check ret == 0.
Nine of them the matrix already answers in-process and more precisely --
what the failure message says, what the operands saw, how often each ran.

Two are worth more than a deletion, so they move rather than vanish:

  assert not (a and ((a := False) is False))

reads back as an introspection case, and the composite chain

  assert a and True and ((a := False) is False) and (a is False) and ...

as an evaluation-order one.  That second is why the deletion is not just
tidying: it passes on main, and it passes there because of the bug.  Main
rewrites the walrus target to an internal temp and substitutes the stored
NamedExpr into every later read of the name -- including the next
statement, so the `assert a is None` that is supposed to verify the outcome
re-runs the walrus and creates it.  Asked through returned values instead
of ret == 0, main fails the case.

TestIssue14445 loses the four tests that restate matrix single-evaluation
entries this PR already adds, and keeps the two reproducers from the issue.

One test survives in place: nothing the rewriter stores may outlive a
statement, which needs two tests in one module to observe and so cannot be
said in-process.

Co-authored-by: Claude Opus 5 <ai@anthropic.com>
Co-authored-by: Claude Code <ai@anthropic.com>
…14445-assert-walrus

fix(rewrite): prevent walrus operator double evaluation in assertions
@bluetech
bluetech force-pushed the cursor/max-warnings-toml-int-6b43 branch 2 times, most recently from 0b7c71f to de7bf7f Compare September 5, 2026 18:51
The max_warnings option was registered without a type (defaulting to
'string'), so integer values in native TOML config raised a TypeError.
It is now registered with type=int | str, accepting both int and string
values in TOML while keeping the string form working for backward
compatibility. An explicit integer 0 is distinguished from the unset
default.

Co-authored-by: Cursor Grok 4.6 <cursoragent@cursor.com>
@bluetech
bluetech force-pushed the cursor/max-warnings-toml-int-6b43 branch from de7bf7f to ee08c00 Compare September 5, 2026 19:07
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.

max_warnings rejects the integer shown in the native TOML documentation

3 participants