[py] adopt the worthwhile ruff 0.16 rule families (tranches 1 and 2) - #17992
Open
AutomatedTester wants to merge 6 commits into
Open
[py] adopt the worthwhile ruff 0.16 rule families (tranches 1 and 2)#17992AutomatedTester wants to merge 6 commits into
AutomatedTester wants to merge 6 commits into
Conversation
Tranche 1 of #17989. Adds the rule families whose findings are pure local rewrites, and fixes the 20 findings so //py:ruff-check stays green. Adopted as whole families (verified clean at full-family scope): C4, FLY, W. Adopted as individual rules, because the rest of their family is far noisier than the subset ruff 0.16 enables by default: FURB122, FURB167, FURB188, FURB192, PERF102. For scale, whole-family PLR is 332 findings (251 of them PLR2004) against 4 in the default set, and whole-family FURB/PERF are 29/19 against 10/2 -- so select entries have to be rule codes there, not family names. Fixes: - FURB188 x4 generate_bidi.py: removeprefix/removesuffix - FURB122 x2 generate.py, firefox_profile.py: writelines - FURB167 x2 update_cddl.py: re.S -> re.DOTALL - FURB192 x2 pinned_browsers.py, update_cdp.py: sorted(...)[-1] -> max(...) - C400 x2, C408 x2, C417, C419, C420: comprehension rewrites - PERF102 x2 cdp.py: .items() -> .values() - FLY002 remote_server_tests.py: static join -> literal Note on FURB192: sorted(...)[-1] and max(...) disagree when two entries tie on the sort key -- a stable sort returns the last tied element, max the first. Both call sites pick the newest chrome-for-testing build within a milestone, where version strings are unique, so this is inert in practice. Calling it out because it is the only behavioural change in the commit. The comprehension rewrites in webdriver.py touch _wrap_value/_unwrap_value and the one in _script_handlers.py feeds a BiDi preload script, so those were checked for equivalence against the pre-change code over a generated input corpus before being applied. bazel test --test_size_filters=small //py:unit passes 32/32; //py:ruff-check and //py:ruff-format --check are clean. Deliberately not adopted here: FURB110 (18, if-exp -> or, a readability regression as often as not) and the rest of PERF (PERF401/403 comprehension rewrites, PERF203 try-except-in-loop), which are loop-shape changes rather than mechanical ones.
Tranche 1 of #17989, split out from the mechanical-rewrite commit because it is the only part that changes git file modes. Six files disagreed with themselves about whether they are directly executable. All six are py_binary srcs, invoked through Bazel (./go update_cddl -> bazel run //scripts:update_cddl) or with an explicit interpreter (py/tox.ini uses {envpython} ./generate_api_module_listing.py), so none of them ever relies on its shebang. Resolved all six the same way -- not directly executable: - chmod -x (100755 -> 100644), which clears EXE002: py/generate_api_module_listing.py, py/generate_bidi.py, py/run_mypy.py, py/run_sphinx_autogen.py - drop the now-unused shebang, which clears EXE001: scripts/update_cddl.py, scripts/update_docfx.py //scripts:update_cddl, //scripts:update_docfx, //py:generate-api-listing, //py:generate_bidi, //py:mypy and //py:sphinx-autogen all still build.
…LR0133 (#17989) Tranche 1 of #17989. All four PLR0133 (comparison-of-constant) findings are the same shape in visibility_tests.py: try: element.click() assert 1 == 0, "should have thrown an exception" except (ElementNotVisibleException, ElementNotInteractableException): pass which is pytest.raises spelled the long way round, and reports "assert 1 == 0" rather than "DID NOT RAISE" when the expectation is not met. Converted all four to a with pytest.raises(...) block. PT011 and PT012 are already in extend-ignore, so the tuple form and the trailing assert in the send_keys test need no further exemption. Adds PLR0133 as an individual rule rather than the PLR family: whole-family PLR is 332 findings (251 of them PLR2004 magic-value-comparison), and it also carries PLR0913/PLR0917 (too-many-arguments / -positional-arguments, 45 hits) which land on public constructor signatures the API-compatibility invariant protects. //py:test/selenium/webdriver/common/visibility_tests-chrome passes 15/15, with all four converted tests confirmed as run rather than skipped.
Tranche 1 of #17989. Both families are small and clean at whole-family scope, so they go in as bare family names. PIE790 x5 -- dropped the unnecessary `pass` after a docstring in cdp.py, webdriver.py (start_client/stop_client), webelement.py (BaseWebElement) and the test webserver. Safe autofix. The remaining four findings are false positives, suppressed with a `noqa` and a reason rather than "fixed": - PIE796 x3, generate_bidi.py CddlType: the duplicate Enum values are deliberate aliases (TEXT->TSTR, INT/NINT->UINT), and get_annotation already resolves them through `__members__`, which includes aliases. The bug this rule would have caught was fixed in 9d0ae9e; what is left is the intended pattern, so the comment now says so explicitly. - PLE0605 x1, webdriver/__init__.py: `__all__ = sorted(_LAZY_IMPORTS.keys())`. sorted() returns a list, so this is correct at runtime; the rule only accepts a literal. The usual argument for a literal is static readability, but __init__.pyi already re-exports every name explicitly with `as` aliases, so type checkers never read this line. Deriving __all__ from _LAZY_IMPORTS is what stops the two from drifting at runtime, so it stays. Note for reviewers: nothing currently asserts that __init__.pyi's re-export list and _LAZY_IMPORTS agree. That gap is worth its own change; it is not introduced here. Verified: regenerating the BiDi sources with this generator produces output byte-identical to trunk's across all 22 files, apart from the one _script_handlers.py line changed deliberately in the mechanical commit. //py:unit passes 32/32, including BaseWebElement's ABC subclass/register behaviour and the start_client/stop_client no-op hooks whose `pass` was removed.
Tranche 2 of #17989. Adopts B as a family rather than a list of rule codes, so that new bugbear rules in a future ruff release surface as one review decision instead of arriving silently. Whole-family B is 59 findings against the 33 ruff 0.16 enables by default, so four rules are listed in extend-ignore. Fixed (7): - B009 x5 / B010 x1 -- constant getattr/setattr in print_page_options.py, proxy.py and timeouts.py. Safe autofix. proxy.py's is inside a descriptor's __set__; proxyType is a plain class attribute rather than another descriptor, so direct assignment is equivalent, and the dynamic setattr(obj, self.name, ...) on the next line is left alone. - B006 x1 -- javascript/private/gen_file.py `def main(argv=[])`. The default is unreachable in practice (the only caller is `main(sys.argv)`) and would IndexError on argv[1] if it were ever used, so it is dropped rather than replaced with a None sentinel. - B018 x1 -- click_scrolling_tests.py accessed `.size` for its side effect to prove that reading it does not scroll; now `_ = ...` so the intent is explicit. Carved out via per-file-ignores (18): B018 under py/test/** and in py/conftest.py. 17 of the 19 findings are a property access inside `with pytest.raises(...)`, which is the idiomatic way to assert that reading a property raises; the 19th is the driver-liveness probe in conftest.py. Note that conftest.py is not under py/test/**, so the pattern suggested on the issue would have missed it. Both globs resolve against the working directory rather than this file, which is why they are spelled "py/..." -- there is now a comment saying so, because they silently stop matching if ruff is invoked from inside py/. Deferred via extend-ignore: - B904 (17) raise-without-from-inside-except. This is the highest-value rule in the whole 184-finding set for a library -- it materially improves user tracebacks -- but it is 16 findings in py/selenium and deserves its own change rather than being bundled into a lint-config commit. - B017 (7) pytest.raises(Exception). Narrowing each site needs the actual exception type confirmed against a live browser (three are BiDi tests, where 8cbf4bf's typed errors make narrowing most valuable), so it is deferred rather than guessed. - B028 (4), B007 (3), B024 (2). //py:unit passes 32/32. //javascript/chrome-driver:header, :source and //javascript/ie-driver:header build clean, covering the gen_file.py change.
Tranche 2 of #17989, completing the bugbear adoption by dropping B017 from extend-ignore. Each exception type was confirmed by running the Bazel test target with a deliberately wrong assertion and reading the type back out of the failure, rather than by guessing: - proxy_tests.py x2 -> ValueError, from Proxy._verify_proxy_type_compatibility. - executing_javascript_tests.py -> TypeError. Passing the driver as a script argument is rejected by json.dumps client-side, before any command is sent. - _bidi/browsing_context_tests.py x2 -> NoSuchFrameException, for get_tree on a closed context. - _bidi/script_tests.py -> NoSuchHandleException, for call_function on a disowned handle. The two BiDi files are where this is most worth having: 8cbf4bf added typed errors for BiDi wire error codes, so these assertions now pin the exact code the spec requires instead of accepting anything at all. quit_tests.py needed a tuple. Which error surfaces after driver.quit() depends on what is left listening -- against a local driver the process is gone and urllib3 raises MaxRetryError, while against a Grid the server is still up and the session lookup fails with InvalidSessionIdException. Both were confirmed by running quit_tests-chrome and quit_tests-chrome-remote. Worth noting for a separate change: MaxRetryError is a raw urllib3 error escaping the public API, and arguably ought to be wrapped in a WebDriverException. Verified: proxy_tests-chrome, quit_tests-chrome, quit_tests-chrome-remote and executing_javascript_tests-chrome all pass. In the BiDi suites the four narrowed tests passed 6/6 across repeat runs; the one failure in those files is test_activate_browsing_context, which fails roughly one run in two both here and on trunk (asynchronous focus handover) and is unrelated to this change. //py:ruff-check clean, //py:unit 32/32.
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.
Part of #17989.
Implements tranche 1 ("adopt now") and tranche 2 ("adopt with scoping") from #17989. Six commits, one per decision, each leaving
//py:ruff-checkand//py:ruff-format --checkclean and//py:unitat 32/32, so any of them can be dropped independently.select[py] enforce the mechanical ruff rule families from ruff 0.16C4FLYW+FURB122/167/188/192PERF102[py] settle shebang/exec-bit consistencyEXE[py] replace the assert 1 == 0 idiom with pytest.raisesPLR0133[py] enforce ruff PIE and PLEPIEPLE[py] enforce ruff bugbear (B) with carve-outsB+ 4extend-ignore[py] narrow pytest.raises(Exception) and enforce B017B017ignore)Three things that differ from the issue
1.
selecttakes whole families, not ruff's default subset. This is the important one. The issue's "adopting a family means adding it toselect" would enforce far more than the numbers in its table:PLRPLR2004)BFURBPERFPYITranches 1 + 2 as bare family names is 479 findings, not ~81. So
PLR,PERFandFURBgo in as explicit rule codes here, andBgoes in as a family plus a matchingextend-ignoreblock — family form on purpose, so new bugbear rules in a future ruff release surface as one review decision instead of silently.2. Neither "genuine bug" cited for tranche 1 is a bug.
PLE0605is__all__ = sorted(_LAZY_IMPORTS.keys())—sorted()returns a list, and__init__.pyialready re-exports every name explicitly, so nothing static reads that line.PIE796's duplicate enum values ingenerate_bidi.pyare deliberate aliases, and the bug they used to cause was fixed in9d0ae9e68f. Both are nownoqa+ a reason rather than "fixed".3. The
B018carve-out needs one more entry."py/test/**" = ["B018"]takes 19 findings to 1; the survivor is the driver-liveness probe inpy/conftest.py, which is not underpy/test/**. Both globs are also resolved against the working directory rather thanpyproject.toml, so they only work becausepy/private/ruff.pychdirs to the workspace root — there is a comment saying so, since they fail silently if ruff is run from insidepy/.Also worth knowing:
//py:ruff-checklintspy scripts common dotnet java javascript rb, so this touchesscripts/andjavascript/private/gen_file.pytoo. No runtime cross-binding impact.Verification
//py:unit32/32 on every commit.visibility_tests-chrome15/15, with all four converted tests confirmed as run rather than skipped.PIE/PLEcommit touches the BiDi generator, so the BiDi sources were regenerated and diffed against trunk: byte-identical across all 22 files, apart from the one_script_handlers.pyline changed deliberately in the first commit.B017exception types were each confirmed by running the Bazel target with a deliberately wrong assertion and reading the real type back, not guessed:ValueError(proxy),TypeError(script argument rejected client-side byjson.dumps),NoSuchFrameException(closed context),NoSuchHandleException(disowned handle).quit_testsneeded a tuple — a local driver givesMaxRetryErrorbecause the process is gone, a Grid givesInvalidSessionIdExceptionbecause the session is — verified against bothquit_tests-chromeandquit_tests-chrome-remote.webdriver.py(_wrap_value/_unwrap_value) and thedict.fromkeyschange feeding a BiDi preload script were checked for equivalence against the pre-change code over a generated input corpus before being applied.Deliberately not included
B904(17, mostlypy/selenium) —raise ... from errinsideexcept. For a library this is arguably the highest-value rule in the whole 184-finding set, and the issue buries it inside theBrow. Inextend-ignorehere; wants its own PR.B028(4),B007(3),B024(2).PYI021(8) should join the issue's decline list: every hit is the "This stub file is necessary for type checkers…" docstring in the__init__.pyifiles, which the rule wants deleted.SIM/TRY/G) untouched. Of note,SIM115— the one the issue calls substantive — is mostly a false positive:service.py:63andgen_file.pyhold intentionally process-lifetime handles, andservice.pyhas an_owns_log_outputflag that exists precisely because it cannot be awithblock.G201(5) looks worth adopting on its own.Follow-ups found along the way
__init__.pyi's re-export list and_LAZY_IMPORTSagree.urllib3.exceptions.MaxRetryErrorescapes the public API afterdriver.quit(); arguably should be wrapped in aWebDriverException._bidi/browsing_context_tests.py::test_activate_browsing_contextis flaky — fails roughly one run in two on trunk as well as on this branch (asynchronous focus handover). Unrelated to this PR; happy to file it separately.Note on measurement
The issue's table says 192 findings; the tree is at 184 now (
W1→0,B35→33,SIM28→26,PLW16→13), so the numbers above were all re-measured against8cbf4bfa2dwith the pinned ruff 0.16.5 rather than taken from the issue.Disclosure per CONTRIBUTING.md: substantial parts of this PR were written with AI assistance (Claude Code). All of it has been reviewed, and every claim above was verified by running the Bazel targets named.
Co-Authored-By: Copse noreply@copse.dev
Copse-Models: acp:claude-agent-acp#opus[1m]