From 4f7f02e2ebe00f003a3b0826f3402b39e8516ccc Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 19 Aug 2026 22:03:39 +0200 Subject: [PATCH 1/3] [refactor] Drop a duplicate key in 'LOGLEVEL_COLOROPTS' 'logging.WARN' is another name for 'logging.WARNING', not a separate level, so the dict literal listed the same integer key twice and only ever held one of the two entries. LOG009 pointed this out. Co-Authored-By: Claude Opus 5 (1M context) --- src/_pytest/logging.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index b24a00715db..51c4954fb38 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -85,7 +85,6 @@ class ColoredLevelFormatter(DatetimeFormatter): logging.CRITICAL: {"red"}, logging.ERROR: {"red", "bold"}, logging.WARNING: {"yellow"}, - logging.WARN: {"yellow"}, logging.INFO: {"green"}, logging.DEBUG: {"purple"}, logging.NOTSET: set(), From 0a8c7c670c6fba1a4b13e44f4692dcc3c67492cf Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 19 Aug 2026 22:03:50 +0200 Subject: [PATCH 2/3] [refactor] Drop a redundant 'tuple()' call in the rewriter The argument is already a tuple, built by unpacking the previous scope, so wrapping it in 'tuple()' copies it for nothing. C409 pointed this out. Co-Authored-By: Claude Opus 5 (1M context) --- src/_pytest/assertion/rewrite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 362c93d7253..3f44ce72fb4 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -723,7 +723,7 @@ def run(self, mod: ast.Module) -> None: while nodes: node = nodes.pop() if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef): - self.scope = tuple((*self.scope, node)) + self.scope = (*self.scope, node) nodes.append(_SCOPE_END_MARKER) if node == _SCOPE_END_MARKER: self.scope = self.scope[:-1] From 2c26a4628ad01d1917cf8a7e315cc54b8c61a63c Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 19 Aug 2026 22:04:01 +0200 Subject: [PATCH 3/3] [ruff] Enable ten more rules the code already satisfies FA102, N999, PGH005, PTH210, S112, LOG009 and the source half of C409 need no change now that the two problems they found are fixed. The remaining four only need a one-line exemption, because the violation is the thing under test: SIM202 on the 'approx' operator overloading check, SIM222 and SIM223 in the assertion rewriting tests, C409 where 'tuple([1, 2])' deliberately builds two equal but distinct tuples ('assert v1 is not v2' on the next line). Every exemption is a 'noqa' on the offending line rather than an entry in 'lint.ignore' or 'per-file-ignores', so the rules keep applying to the rest of those files. Exclude never-invoked assert helpers from coverage Co-Authored-By: Claude Opus 5 (1M context) --- pyproject.toml | 13 ------------- testing/python/approx.py | 2 +- testing/python/fixtures.py | 2 +- testing/test_assertrewrite.py | 8 ++++---- 4 files changed, 6 insertions(+), 19 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5cc86d60857..076026f7689 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,7 +147,6 @@ lint.ignore = [ "BLE001", # Do not catch blind exception # flake8-comprehensions ignore "C408", # Unnecessary `dict()`/`list()`/`tuple()` call (rewrite as a literal) - "C409", # Unnecessary list literal passed to `tuple()` (rewrite as a tuple literal) # pydocstyle ignore "D100", # Missing docstring in public module "D101", # Missing docstring in public class @@ -170,19 +169,12 @@ lint.ignore = [ # pytest can do weird low-level things, and we usually know # what we're doing when we use type(..) is ... "E721", # Do not compare types, use `isinstance()` - # flake8-future-annotations ignore - "FA102", # Missing `from __future__ import annotations`, but uses PEP 585/604 syntax # flynt ignore "FLY002", # Consider an f-string instead of string join # flake8-implicit-str-concat ignore "ISC004", # Unparenthesized implicit string concatenation in collection # flake8-logging ignore - "LOG009", # Use of undocumented `logging.WARN` constant "LOG015", # Call on root logger - # pep8-naming ignore - "N999", # Invalid module name - # pygrep-hooks ignore - "PGH005", # Mock method should be called # pylint ignore "PLC0105", # `TypeVar` name "E" does not reflect its covariance; "PLC0414", # Import alias does not rename original package @@ -208,14 +200,12 @@ lint.ignore = [ "PT031", # `pytest.warns()` block should contain a single simple statement # flake8-use-pathlib ignore "PTH124", # `py.path` is in maintenance mode, use `pathlib` instead - "PTH210", # Invalid suffix passed to `.with_suffix()` # ruff ignore "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` "RUF061", # Use context-manager form of `pytest.raises()` # flake8-bandit ignore "S102", # Use of `exec` detected "S110", # `try`-`except`-`pass` detected, consider logging the exception - "S112", # `try`-`except`-`continue` detected, consider logging the exception # flake8-simplify ignore "SIM102", # Use a single `if` statement instead of nested `if` statements "SIM103", # Return the condition directly @@ -223,10 +213,7 @@ lint.ignore = [ "SIM115", # Use a context manager for opening files "SIM117", # Use a single `with` statement with multiple contexts instead of nested `with` statements "SIM201", # Use `!=` instead of `not ... == ...` - "SIM202", # Use `==` instead of `not ... != ...` "SIM211", # Use `not ...` instead of `False if ... else True` - "SIM222", # Use the simplified expression instead of `... or True` - "SIM223", # Use the simplified expression instead of `... and False` # tryceratops ignore "TRY002", # Create your own exception "TRY004", # Prefer `TypeError` exception for invalid type diff --git a/testing/python/approx.py b/testing/python/approx.py index c635297917c..b63a1c7a051 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -425,7 +425,7 @@ def test_mixed_sequence(self, assert_approx_raises_regex) -> None: def test_operator_overloading(self): assert 1 == approx(1, rel=1e-6, abs=1e-12) - assert not (1 != approx(1, rel=1e-6, abs=1e-12)) + assert not (1 != approx(1, rel=1e-6, abs=1e-12)) # noqa: SIM202 assert 10 != approx(1, rel=1e-6, abs=1e-12) assert not (10 == approx(1, rel=1e-6, abs=1e-12)) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index e4334af2145..c0b49948152 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4542,7 +4542,7 @@ class TestParamValueKey: def test_equal_hashable_values(self) -> None: # Build equal-but-not-identical values to exercise the ``==`` path # rather than the identity shortcut. - v1, v2 = tuple([1, 2]), tuple([1, 2]) + v1, v2 = tuple([1, 2]), tuple([1, 2]) # noqa: C409 assert v1 is not v2 k1, k2 = ParamValueKey(v1, 0), ParamValueKey(v2, 1) assert k1 == k2 diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 12e12449693..ef038383ed1 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -675,8 +675,8 @@ def f11() -> None: getmsg(f11, must_pass=True) def test_short_circuit_evaluation(self) -> None: - def f1() -> None: - assert True or explode # type: ignore[name-defined,unreachable] # noqa: F821 + def f1() -> None: # pragma: no cover + assert True or explode # type: ignore[name-defined,unreachable] # noqa: F821,SIM222 getmsg(f1, must_pass=True) @@ -725,8 +725,8 @@ def f2() -> None: assert getmsg(f2) == "assert not (5 % 4)" def test_boolop_percent(self) -> None: - def f1() -> None: - assert 3 % 2 and False + def f1() -> None: # pragma: no cover + assert 3 % 2 and False # noqa: SIM223 assert getmsg(f1) == "assert ((3 % 2) and False)"