Skip to content

Commit bfbf2cc

Browse files
authored
Merge pull request #3934 from PolicyEngine/ruff-complexity-tests-scripts
Refactor tests, scripts and the rules tool, then select every ruff rule (ALL)
2 parents fc3ee33 + a04d681 commit bfbf2cc

61 files changed

Lines changed: 4971 additions & 1609 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/bump_version.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
from pathlib import Path
66
from typing import TextIO
77

8+
# A fragment named "<issue>.<category>.md" has at least two dot-separated stem parts.
9+
STEM_PARTS_WITH_CATEGORY = 2
10+
811

912
def _report(message: str, *, file: TextIO | None = None) -> None:
1013
"""Print one line of this command's output, to stdout unless ``file`` is given."""
@@ -40,7 +43,7 @@ def infer_bump(changelog_dir: Path) -> str:
4043
categories = {f.suffix.lstrip(".") for f in fragments}
4144
for f in fragments:
4245
parts = f.stem.split(".")
43-
if len(parts) >= 2:
46+
if len(parts) >= STEM_PARTS_WITH_CATEGORY:
4447
categories.add(parts[-1])
4548

4649
if "breaking" in categories:

‎.github/scripts/check_updates.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import argparse
1313
import re
1414
import sys
15+
from http import HTTPStatus
1516
from typing import TypedDict
1617

1718
import requests
@@ -38,7 +39,7 @@ def fetch_changelog(package: str) -> str | None:
3839
for branch in ("main", "master"):
3940
url = f"https://raw.githubusercontent.com/{repo}/{branch}/CHANGELOG.md"
4041
resp = requests.get(url, timeout=30)
41-
if resp.status_code == 200:
42+
if resp.status_code == HTTPStatus.OK:
4243
return resp.text
4344
return None
4445

‎.github/scripts/verify_observability_collector.py‎

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import time
99
import uuid
1010
from datetime import UTC, datetime
11+
from http import HTTPStatus
1112
from typing import NamedTuple
1213
from urllib.error import HTTPError
1314
from urllib.parse import quote, urlencode, urlparse
@@ -71,6 +72,14 @@ class VerificationProbe(NamedTuple):
7172
started_at: float
7273

7374

75+
class CloudReadback(NamedTuple):
76+
"""The Google Cloud project, metric location and token for reading telemetry."""
77+
78+
project_id: str
79+
metric_location: str
80+
access_token: str
81+
82+
7483
def _authority(endpoint: str) -> str:
7584
parsed = urlparse(endpoint)
7685
if parsed.scheme != "https" or not parsed.hostname:
@@ -207,7 +216,7 @@ def _get_json(url: str, access_token: str) -> dict[str, object] | None:
207216
with urlopen(request, timeout=15) as response:
208217
return json.load(response)
209218
except HTTPError as error:
210-
if error.code == 404:
219+
if error.code == HTTPStatus.NOT_FOUND:
211220
return None
212221
detail = error.read().decode("utf-8", errors="replace")
213222
msg = f"Google Cloud verification request failed ({error.code}): {detail}"
@@ -268,12 +277,8 @@ def _metric_available(
268277

269278
def _wait_for_delivery(
270279
*,
271-
project_id: str,
272-
trace_id: str,
273-
verification_id: str,
274-
metric_location: str,
275-
access_token: str,
276-
started_at: float,
280+
readback: CloudReadback,
281+
probe: VerificationProbe,
277282
timeout_seconds: float,
278283
poll_seconds: float = 5.0,
279284
) -> None:
@@ -282,14 +287,16 @@ def _wait_for_delivery(
282287
metric_found = False
283288
while True:
284289
if not trace_found:
285-
trace_found = _trace_available(project_id, trace_id, access_token)
290+
trace_found = _trace_available(
291+
readback.project_id, probe.trace_id, readback.access_token
292+
)
286293
if not metric_found:
287294
metric_found = _metric_available(
288-
project_id,
289-
verification_id,
290-
metric_location,
291-
access_token,
292-
started_at,
295+
readback.project_id,
296+
probe.verification_id,
297+
readback.metric_location,
298+
readback.access_token,
299+
probe.started_at,
293300
)
294301
if trace_found and metric_found:
295302
return
@@ -312,9 +319,7 @@ def verify(
312319
endpoint: str,
313320
token: str,
314321
*,
315-
project_id: str,
316-
metric_location: str,
317-
access_token: str,
322+
readback: CloudReadback,
318323
timeout_seconds: float = 15.0,
319324
delivery_timeout_seconds: float = 600.0,
320325
) -> VerificationProbe:
@@ -361,16 +366,11 @@ def verify(
361366
else:
362367
msg = "collector unexpectedly accepted application logs"
363368
raise RuntimeError(msg)
369+
probe = VerificationProbe(verification_id, trace_id, started_at)
364370
_wait_for_delivery(
365-
project_id=project_id,
366-
trace_id=trace_id,
367-
verification_id=verification_id,
368-
metric_location=metric_location,
369-
access_token=access_token,
370-
started_at=started_at,
371-
timeout_seconds=delivery_timeout_seconds,
371+
readback=readback, probe=probe, timeout_seconds=delivery_timeout_seconds
372372
)
373-
return VerificationProbe(verification_id, trace_id, started_at)
373+
return probe
374374

375375

376376
def main() -> None:
@@ -387,9 +387,11 @@ def main() -> None:
387387
probe = verify(
388388
args.endpoint,
389389
os.environ["COLLECTOR_ID_TOKEN"],
390-
project_id=args.project_id,
391-
metric_location=args.metric_location,
392-
access_token=os.environ["GOOGLE_OAUTH_ACCESS_TOKEN"],
390+
readback=CloudReadback(
391+
project_id=args.project_id,
392+
metric_location=args.metric_location,
393+
access_token=os.environ["GOOGLE_OAUTH_ACCESS_TOKEN"],
394+
),
393395
)
394396
print(
395397
"Verified collector ingestion "

‎changelog.d/ruff-all.changed.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Selected every ruff rule (ALL) now that each rule family passes, ignoring only COM812 and ISC001, which conflict with ruff format, and D203 and D213, which conflict with the kept D211 and D212; the ruff-lint rule in the registry now names ALL, and published schemas and the HTTP API are unchanged.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Refactored the tests, the repository scripts and the rules tool under ruff's complexity rules (C901, PLR): long tests and script functions are split into named steps or helpers, related arguments travel as small value objects, and magic numbers are named, with characterization tests pinning the scripts' and rules tool's behaviour first; command output, test coverage and published schemas are unchanged.

‎docs/engineering/skills/rules-registry.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,12 @@ fail the guard yet.
115115

116116
## Baselines
117117

118-
- **Ruff is never ratcheted.** Adopting a ruff rule family is one standalone PR
119-
that changes the select list and fixes every resulting violation. Don't use
120-
`--add-noqa`. A finding that is kept gets an inline `# noqa: CODE # reason`,
121-
except in the files below.
118+
- **Ruff is never ratcheted.** `select = ["ALL"]`, so every ruff rule applies
119+
except the four listed in `ignore` (two formatter conflicts and one of each
120+
mutually exclusive docstring pair) and the registered per-file ignores. When
121+
a ruff upgrade adds rules, the upgrade PR fixes every new violation. Don't
122+
use `--add-noqa`. A finding that is kept gets an inline
123+
`# noqa: CODE # reason`, except in the files below.
122124
- Other enforcers (mypy, Pyright, Import Linter, custom guards) may start
123125
`ratcheted` with a baseline file of tolerated violations. Baselines may only
124126
shrink: the guard compares counts against the base branch.

‎docs/generated/rules.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ flowchart LR
296296
| `api.quality.environment-variables-load` | Required environment variables and credentials load in a deployed-like environment. | mechanical | enforced | yes | CI job `test_env_vars` | `pr.yml:test_env_vars` | — |
297297
| `api.quality.mypy-v2` | Shared query models, FastAPI dependencies, v2 routes and services, and the user sign-in and account-deletion libraries pass mypy with untyped definitions disallowed. | mechanical | enforced | yes | mypy | `pr.yml:typecheck-v2` | — |
298298
| `api.quality.ruff-format` | All Python files are formatted with ruff format. | mechanical | enforced | yes | ruff format | `pr.yml:lint` | — |
299-
| `api.quality.ruff-lint` | All Python code passes ruff check with every rule family selected in pyproject.toml, with no ratchet; families are added one themed group at a time until the selection is ALL. | mechanical | enforced | yes | ruff check (A, ANN, ARG, ASYNC, B, BLE, C4, D, DTZ, E, E4, E7, E9, EM, ERA, EXE, F, FAST, FBT, FIX, FLY, FURB, G, I, INP, LOG, N, PD, PERF, PGH, PIE, PLC, PLW, PT, PTH, PYI, RET, RSE, RUF, S, SIM, SLF, T20, TC, TD, TID253, TRY, UP, W) | `pr.yml:lint` | — |
299+
| `api.quality.ruff-lint` | All Python code passes ruff check with every ruff rule selected (ALL), with no ratchet, except the ignores in pyproject.toml: COM812 and ISC001, which conflict with ruff format, and D203 and D213, which conflict with the kept D211 and D212. | mechanical | enforced | yes | ruff check (ALL) | `pr.yml:lint` | — |
300300
| `api.quality.runtime-evaluated-models-resolve` | Every package model is one ruff treats as runtime-evaluated (a base resolves, through the import in its own module, to a name in runtime-evaluated-base-classes, or is a same-module class that does), and every Pydantic model and SQLAlchemy mapper resolves its annotations once the package is imported. | mechanical | enforced | yes | pytest `tests/unit/test_runtime_evaluated_annotations.py` | `pr.yml:test` | **unproven** |
301301
| `api.quality.tests-pass` | The unit, contract and to-refactor test suites pass. | mechanical | enforced | yes | CI job `test` | `pr.yml:test` | — |
302302
| `api.quality.tests-ruff-exemptions` | Test files may use pytest's bare assert, need no docstrings, annotations or named constants for magic numbers, and may reach into private members, live outside packages and import inside a test (S101, D1, ANN, PLR2004, SLF001, INP001, PLC0415). Their fakes may name methods after the interface they replace, such as a gRPC stub's Export (N802). They may also use fake credentials, naive datetimes, subprocess calls to repository scripts, SQL built from strings and prints (S105, S106, DTZ001, DTZ011, S603, S607, S608, T201), may import the lazily loaded packages at module level (TID253), and may leave a stand-in's arguments unused (ARG001, ARG002, ARG005). Every other ruff rule applies to tests. | mechanical | enforced | yes | ruff per-file ignore of ANN, ARG001, ARG002, ARG005, D1, DTZ001, DTZ011, INP001, N802, PLC0415, PLR2004, S101, S105, S106, S603, S607, S608, SLF001, T201, TID253 for `**/tests/**` | `pr.yml:lint` | — |
@@ -322,7 +322,7 @@ flowchart LR
322322
r_api_quality_ruff_format("api.quality.ruff-format (enforced)")
323323
e_ruff_format["ruff format"]
324324
r_api_quality_ruff_lint("api.quality.ruff-lint (enforced)")
325-
e_ruff_check__A__ANN__ARG__ASYNC__B__BLE__C4__D__DTZ__E__E4__E7__E9__EM__ERA__EXE__F__FAST__FBT__FIX__FLY__FURB__G__I__INP__LOG__N__PD__PERF__PGH__PIE__PLC__PLW__PT__PTH__PYI__RET__RSE__RUF__S__SIM__SLF__T20__TC__TD__TID253__TRY__UP__W_["ruff check (A, ANN, ARG, ASYNC, B, BLE, C4, D, DTZ, E, E4, E7, E9, EM, ERA, EXE, F, FAST, FBT, FIX, FLY, FURB, G, I, INP, LOG, N, PD, PERF, PGH, PIE, PLC, PLW, PT, PTH, PYI, RET, RSE, RUF, S, SIM, SLF, T20, TC, TD, TID253, TRY, UP, W)"]
325+
e_ruff_check__ALL_["ruff check (ALL)"]
326326
r_api_quality_runtime_evaluated_models_resolve("api.quality.runtime-evaluated-models-resolve (enforced)")
327327
e_pytest_tests_unit_test_runtime_evaluated_annotations_py["pytest tests/unit/test_runtime_evaluated_annotations.py"]
328328
j_CI_pr_yml_test["CI pr.yml:test"]
@@ -345,8 +345,8 @@ flowchart LR
345345
e_mypy --> j_CI_pr_yml_typecheck_v2
346346
r_api_quality_ruff_format --> e_ruff_format
347347
e_ruff_format --> j_CI_pr_yml_lint
348-
r_api_quality_ruff_lint --> e_ruff_check__A__ANN__ARG__ASYNC__B__BLE__C4__D__DTZ__E__E4__E7__E9__EM__ERA__EXE__F__FAST__FBT__FIX__FLY__FURB__G__I__INP__LOG__N__PD__PERF__PGH__PIE__PLC__PLW__PT__PTH__PYI__RET__RSE__RUF__S__SIM__SLF__T20__TC__TD__TID253__TRY__UP__W_
349-
e_ruff_check__A__ANN__ARG__ASYNC__B__BLE__C4__D__DTZ__E__E4__E7__E9__EM__ERA__EXE__F__FAST__FBT__FIX__FLY__FURB__G__I__INP__LOG__N__PD__PERF__PGH__PIE__PLC__PLW__PT__PTH__PYI__RET__RSE__RUF__S__SIM__SLF__T20__TC__TD__TID253__TRY__UP__W_ --> j_CI_pr_yml_lint
348+
r_api_quality_ruff_lint --> e_ruff_check__ALL_
349+
e_ruff_check__ALL_ --> j_CI_pr_yml_lint
350350
r_api_quality_runtime_evaluated_models_resolve --> e_pytest_tests_unit_test_runtime_evaluated_annotations_py
351351
e_pytest_tests_unit_test_runtime_evaluated_annotations_py --> j_CI_pr_yml_test
352352
r_api_quality_tests_pass --> e_CI_job_test

‎pyproject.toml‎

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -99,31 +99,18 @@ namespace-packages = [
9999
]
100100

101101
[tool.ruff.lint]
102-
# Rule families are enabled one themed group per PR, each fixing everything it
103-
# finds (no ratchet), until the select list is "ALL". See
104-
# docs/engineering/skills/rules-registry.md and rules/registry.toml.
105-
select = [
106-
"E4", "E7", "E9", "F",
107-
# Group 1: safe auto-fixes.
108-
"FLY", "FURB", "I", "PIE", "PYI", "RET", "RSE", "UP", "W",
109-
# Group 2: typing-only imports.
110-
"TC",
111-
# Group 3: annotations.
112-
"ANN",
113-
# Group 4: docstrings.
114-
"D",
115-
# Group 5: exceptions and messages.
116-
"BLE", "EM", "TRY",
117-
# Group 6: security, logging and debugging leftovers.
118-
"DTZ", "ERA", "EXE", "FIX", "G", "LOG", "S", "T20", "TD",
119-
# Group 7: structure and imports.
120-
"A", "E", "FAST", "INP", "N", "PLC", "SLF", "TID253",
121-
# Group 8a: arguments, idioms and pytest style.
122-
"ARG", "ASYNC", "B", "C4", "FBT", "PD", "PERF", "PGH", "PLW", "PT", "PTH", "RUF", "SIM",
102+
# Every ruff rule applies, with no ratchet, apart from the ignores below
103+
# (api.quality.ruff-lint). See docs/engineering/skills/rules-registry.md and
104+
# rules/registry.toml.
105+
select = ["ALL"]
106+
ignore = [
107+
# These conflict with ruff format; ruff's documentation says to disable
108+
# them when using the formatter.
109+
"COM812", "ISC001",
110+
# Mutually exclusive pydocstyle pairs: keep D211 (no blank line before a
111+
# class docstring) and D212 (summary on the first line).
112+
"D203", "D213",
123113
]
124-
# Mutually exclusive pydocstyle pairs: keep D211 (no blank line before a class
125-
# docstring) and D212 (summary on the first line).
126-
ignore = ["D203", "D213"]
127114
# Alembic revisions must stay exactly as Alembic generates them
128115
# (api.migration.alembic-autogenerated-only), so ruff does not lint them. This
129116
# is an exclusion, not a file-level exception: per-file-ignores are only for

‎rules/registry.toml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -764,7 +764,7 @@ invocation = "ruff format --check"
764764
[[rule]]
765765
id = "api.quality.ruff-lint"
766766
category = "quality"
767-
statement = "All Python code passes ruff check with every rule family selected in pyproject.toml, with no ratchet; families are added one themed group at a time until the selection is ALL."
767+
statement = "All Python code passes ruff check with every ruff rule selected (ALL), with no ratchet, except the ignores in pyproject.toml: COM812 and ISC001, which conflict with ruff format, and D203 and D213, which conflict with the kept D211 and D212."
768768
rationale = "docs/engineering/skills/rules-registry.md#baselines"
769769
scope = ["**/*.py"]
770770
tier = "mechanical"
@@ -773,7 +773,7 @@ autofix = true
773773
[rule.enforcer]
774774
kind = "ruff"
775775
mode = "lint"
776-
codes = ["A", "ANN", "ARG", "ASYNC", "B", "BLE", "C4", "D", "DTZ", "E", "E4", "E7", "E9", "EM", "ERA", "EXE", "F", "FAST", "FBT", "FIX", "FLY", "FURB", "G", "I", "INP", "LOG", "N", "PD", "PERF", "PGH", "PIE", "PLC", "PLW", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "S", "SIM", "SLF", "T20", "TC", "TD", "TID253", "TRY", "UP", "W"]
776+
codes = ["ALL"]
777777
[[rule.ci]]
778778
workflow = ".github/workflows/pr.yml"
779779
job = "lint"

‎scripts/capture_migration_baseline.py‎

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -75,35 +75,32 @@ def _percentile(values: list[float], percentile: float) -> float | None:
7575

7676
def _request_probe(
7777
client: httpx.Client,
78+
probe: RouteProbe,
7879
*,
79-
name: str,
80-
method: str,
81-
path: str,
82-
expected_statuses: tuple[int, ...],
8380
json_payload: dict | None = None,
8481
) -> tuple[ProbeResult, httpx.Response | None]:
8582
started_at = time.perf_counter()
8683
try:
87-
response = client.request(method, path, json=json_payload)
84+
response = client.request(probe.method, probe.path, json=json_payload)
8885
latency_ms = (time.perf_counter() - started_at) * 1000
8986
return (
9087
ProbeResult(
91-
name=name,
92-
method=method,
93-
path=path,
88+
name=probe.name,
89+
method=probe.method,
90+
path=probe.path,
9491
status_code=response.status_code,
9592
latency_ms=round(latency_ms, 2),
96-
ok=response.status_code in expected_statuses,
93+
ok=response.status_code in probe.expected_statuses,
9794
),
9895
response,
9996
)
10097
except httpx.HTTPError as error:
10198
latency_ms = (time.perf_counter() - started_at) * 1000
10299
return (
103100
ProbeResult(
104-
name=name,
105-
method=method,
106-
path=path,
101+
name=probe.name,
102+
method=probe.method,
103+
path=probe.path,
107104
status_code=0,
108105
latency_ms=round(latency_ms, 2),
109106
ok=False,
@@ -119,13 +116,7 @@ def run_probes(base_url: str, repetitions: int) -> list[ProbeResult]:
119116
with httpx.Client(base_url=base_url.rstrip("/"), timeout=90.0) as client:
120117
for _ in range(repetitions):
121118
for probe in DEFAULT_PROBES:
122-
result, _ = _request_probe(
123-
client,
124-
name=probe.name,
125-
method=probe.method,
126-
path=probe.path,
127-
expected_statuses=probe.expected_statuses,
128-
)
119+
result, _ = _request_probe(client, probe)
129120
results.append(result)
130121
return results
131122

@@ -149,10 +140,12 @@ def run_simulation_gateway_probe(
149140
with httpx.Client(base_url=gateway_url.rstrip("/"), timeout=90.0) as client:
150141
submit_result, submit_response = _request_probe(
151142
client,
152-
name="simulation_gateway_submit",
153-
method="POST",
154-
path="/simulate/economy/comparison",
155-
expected_statuses=(202,),
143+
RouteProbe(
144+
name="simulation_gateway_submit",
145+
method="POST",
146+
path="/simulate/economy/comparison",
147+
expected_statuses=(202,),
148+
),
156149
json_payload=payload,
157150
)
158151
results.append(submit_result)
@@ -182,10 +175,12 @@ def run_simulation_gateway_probe(
182175
while time.perf_counter() < deadline:
183176
poll_result, poll_response = _request_probe(
184177
client,
185-
name="simulation_gateway_poll",
186-
method="GET",
187-
path=f"/jobs/{job_id}",
188-
expected_statuses=(200, 202),
178+
RouteProbe(
179+
name="simulation_gateway_poll",
180+
method="GET",
181+
path=f"/jobs/{job_id}",
182+
expected_statuses=(200, 202),
183+
),
189184
)
190185
last_result = poll_result
191186
results.append(poll_result)

0 commit comments

Comments
 (0)