fix(lint): E402 is on — the ignore meant for main.py had let 11 late imports in elsewhere
ruff.toml ignored E402 everywhere for main.py's logging setup. Measured: 28 hits, of which 11 had nothing to do with it — runner.py's logger line between its imports, models.py importing canonical_email mid-file, a test constant above its imports, and main.py's own third-party and mimetypes imports. Those are moved up. What stays after configure_logging in main.py is marked per statement (noqa E402, logging is configured first), its comment now says why the order matters (a line logged while importing, not the logger's creation). The register row goes, the ruff key whitelist loses ignore, and a late import is red again. The T20 comment counts the third deliberate print (scripts/e2e_seed.py).
This commit is contained in:
+20
-24
@@ -1,38 +1,36 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import math
|
import math
|
||||||
|
import mimetypes
|
||||||
from contextlib import asynccontextmanager, suppress
|
from contextlib import asynccontextmanager, suppress
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, Request
|
from fastapi import FastAPI, HTTPException, Request
|
||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
from fastapi.exceptions import RequestValidationError
|
from fastapi.exceptions import RequestValidationError
|
||||||
|
|
||||||
# Logging is configured HERE, before the route modules are imported, so every logger they create at
|
|
||||||
# import time is already covered. ONE format for the api, the worker, the download child and alembic
|
|
||||||
# alike (app/logs.py) — this replaces the old uvicorn --log-config + the hand-rolled fallback that
|
|
||||||
# used to live here, and uvicorn's access log is replaced by the `http.request` middleware below.
|
|
||||||
from app import logs
|
|
||||||
from app.logs import configure_logging, current_level, log_requests, set_level
|
|
||||||
|
|
||||||
configure_logging("api")
|
|
||||||
|
|
||||||
log = logs.get_logger(__name__)
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from sqlalchemy.exc import DataError
|
from sqlalchemy.exc import DataError
|
||||||
from starlette.middleware.sessions import SessionMiddleware
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
|
|
||||||
from app import bodylimit
|
# Logging is configured HERE, before the app's own modules are imported, so a line one of them logs
|
||||||
from app import collation
|
# WHILE importing already has the format (a logger is configured whenever it is created —
|
||||||
from app import logsink
|
# app/logs.py — but a line emitted before `configure_logging` goes out raw). ONE format for the api,
|
||||||
from app import state
|
# the worker, the download child and alembic alike — this replaces the old uvicorn --log-config +
|
||||||
from app import sysconfig
|
# the hand-rolled fallback that used to live here, and uvicorn's access log is replaced by the
|
||||||
from app.config import settings
|
# `http.request` middleware below. The imports after it are E402 on purpose, each one marked.
|
||||||
from app.db import SessionLocal, engine
|
from app import logs
|
||||||
from app.plex import stream as plex_stream
|
from app.logs import configure_logging, current_level, log_requests, set_level
|
||||||
from app.plex import subtitles as plex_subtitles
|
|
||||||
from app.routes import (
|
configure_logging("api")
|
||||||
|
|
||||||
|
log = logs.get_logger(__name__)
|
||||||
|
from app import bodylimit, collation, logsink, state, sysconfig # noqa: E402 — logging is configured first
|
||||||
|
from app.config import settings # noqa: E402 — logging is configured first
|
||||||
|
from app.db import SessionLocal, engine # noqa: E402 — logging is configured first
|
||||||
|
from app.plex import stream as plex_stream # noqa: E402 — logging is configured first
|
||||||
|
from app.plex import subtitles as plex_subtitles # noqa: E402 — logging is configured first
|
||||||
|
from app.routes import ( # noqa: E402 — logging is configured first
|
||||||
admin,
|
admin,
|
||||||
admin_logs as admin_logs_routes,
|
admin_logs as admin_logs_routes,
|
||||||
audit as audit_routes,
|
audit as audit_routes,
|
||||||
@@ -60,7 +58,7 @@ from app.routes import (
|
|||||||
tags,
|
tags,
|
||||||
version,
|
version,
|
||||||
)
|
)
|
||||||
from app.scheduler import shutdown_scheduler, start_scheduler
|
from app.scheduler import shutdown_scheduler, start_scheduler # noqa: E402 — logging is configured first
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -332,8 +330,6 @@ app.include_router(setup_routes.router)
|
|||||||
|
|
||||||
# Ensure modern image types resolve to the right Content-Type when FileResponse guesses from the
|
# Ensure modern image types resolve to the right Content-Type when FileResponse guesses from the
|
||||||
# filename (the runtime's mimetypes db doesn't always know .webp → it'd fall back to octet-stream).
|
# filename (the runtime's mimetypes db doesn't always know .webp → it'd fall back to octet-stream).
|
||||||
import mimetypes
|
|
||||||
|
|
||||||
mimetypes.add_type("image/webp", ".webp")
|
mimetypes.add_type("image/webp", ".webp")
|
||||||
mimetypes.add_type("image/avif", ".avif")
|
mimetypes.add_type("image/avif", ".avif")
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from sqlalchemy.dialects.postgresql import JSONB, TSVECTOR
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship, validates
|
from sqlalchemy.orm import Mapped, mapped_column, relationship, validates
|
||||||
|
|
||||||
from app.db import Base
|
from app.db import Base
|
||||||
|
from app.utils import canonical_email
|
||||||
|
|
||||||
# Every user-supplied display string in this schema shares one width, and it lives HERE so the
|
# Every user-supplied display string in this schema shares one width, and it lives HERE so the
|
||||||
# columns below are declared with it rather than with a literal: a name a person typed goes into
|
# columns below are declared with it rather than with a literal: a name a person typed goes into
|
||||||
@@ -51,9 +52,6 @@ def display_text(raw: str | None) -> str | None:
|
|||||||
return (raw or "").strip()[:DISPLAY_TEXT_MAX] or None
|
return (raw or "").strip()[:DISPLAY_TEXT_MAX] or None
|
||||||
|
|
||||||
|
|
||||||
from app.utils import canonical_email
|
|
||||||
|
|
||||||
|
|
||||||
# The resume-position columns (VideoState / PlexState.position_seconds) are int4. The checkpoint
|
# The resume-position columns (VideoState / PlexState.position_seconds) are int4. The checkpoint
|
||||||
# routes bound their bodies by this, so an out-of-range position is a 422 rather than a 500.
|
# routes bound their bodies by this, so an out-of-range position is a 422 rather than a 500.
|
||||||
POSITION_MAX_S = 2**31 - 1
|
POSITION_MAX_S = 2**31 - 1
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ from sqlalchemy import func, or_, select, update
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app import logs, progress, quota, sysconfig
|
from app import logs, progress, quota, sysconfig
|
||||||
|
|
||||||
log = logs.get_logger(__name__)
|
|
||||||
from app.db import SessionLocal
|
from app.db import SessionLocal
|
||||||
from app.models import Channel, OAuthToken, Subscription, User
|
from app.models import Channel, OAuthToken, Subscription, User
|
||||||
from app.sync.explore import followed_channel
|
from app.sync.explore import followed_channel
|
||||||
@@ -35,6 +33,8 @@ from app.sync.videos import (
|
|||||||
from app.youtube.client import YouTubeClient, YouTubeError, is_quota_refusal
|
from app.youtube.client import YouTubeClient, YouTubeError, is_quota_refusal
|
||||||
from app.youtube.rss import RssError, fetch_channel_feed
|
from app.youtube.rss import RssError, fetch_channel_feed
|
||||||
|
|
||||||
|
log = logs.get_logger(__name__)
|
||||||
|
|
||||||
# RSS feeds are free and network-bound, so fetch many at once; DB writes stay single-threaded.
|
# RSS feeds are free and network-bound, so fetch many at once; DB writes stay single-threaded.
|
||||||
RSS_POLL_WORKERS = 16
|
RSS_POLL_WORKERS = 16
|
||||||
# Failed channels named in a poll's summary warning; the rest are counted by reason.
|
# Failed channels named in a poll's summary warning; the rest are counted by reason.
|
||||||
|
|||||||
+8
-7
@@ -1,7 +1,8 @@
|
|||||||
# Ruff config for the Siftlode backend. Keeps ruff's default rule set (E4/E7/E9 + F = the
|
# Ruff config for the Siftlode backend. Keeps ruff's default rule set (E4/E7/E9 + F = the
|
||||||
# high-signal pyflakes/pycodestyle checks that catch real dead code + bugs), but ignores E402:
|
# high-signal pyflakes/pycodestyle checks that catch real dead code + bugs), all of it on. E402 was
|
||||||
# a handful of modules (e.g. main.py) deliberately run setup — logging config, sys.path — before
|
# ignored for main.py's logging setup; the ignore had let 11 incidental late imports in elsewhere
|
||||||
# importing route modules, so "module import not at top of file" is intentional there, not cruft.
|
# (E32 review), so it is on now and main.py marks each deliberate one (`# noqa: E402 — <why>`).
|
||||||
|
# `scripts/e2e_seed.py` edits `sys.path` before its imports, which E402 itself allows.
|
||||||
#
|
#
|
||||||
# line-length 100 (ruff's default is 88): matches the frontend prettier printWidth so both halves of
|
# line-length 100 (ruff's default is 88): matches the frontend prettier printWidth so both halves of
|
||||||
# the repo wrap at the same width, and it's the width `ruff format` enforces (gated via
|
# the repo wrap at the same width, and it's the width `ruff format` enforces (gated via
|
||||||
@@ -17,9 +18,10 @@ target-version = "py312"
|
|||||||
# LOG — stdlib logging misuse (`.warn()`, a logger built by hand, `exc_info` on `.exception`)
|
# LOG — stdlib logging misuse (`.warn()`, a logger built by hand, `exc_info` on `.exception`)
|
||||||
# G — the call's shape: %-style args, never an f-string the level filter cannot skip, and
|
# G — the call's shape: %-style args, never an f-string the level filter cannot skip, and
|
||||||
# never `str(exc)` where `exc_info` would carry the traceback
|
# never `str(exc)` where `exc_info` would carry the traceback
|
||||||
# T20 — `print` instead of a log line. Two deliberate ones remain, each with its `# noqa`
|
# T20 — `print` instead of a log line. Three deliberate ones remain, each with its `# noqa`
|
||||||
# and a why: `downloads/runner.py`'s usage line is for whoever typed it, and
|
# and a why: `downloads/runner.py`'s usage line is for whoever typed it, `logsink.py`
|
||||||
# `logsink.py` reports the SINK's own failure, which cannot be logged into the sink.
|
# reports the SINK's own failure, which cannot be logged into the sink, and
|
||||||
|
# `scripts/e2e_seed.py` prints the seeded ids for the e2e run that reads them.
|
||||||
# TRY400 — `.error()` in an `except` where `.exception()` would keep the traceback
|
# TRY400 — `.error()` in an `except` where `.exception()` would keep the traceback
|
||||||
# TRY401 — the exception spelled into the message AND passed as `exc_info` (it is in both)
|
# TRY401 — the exception spelled into the message AND passed as `exc_info` (it is in both)
|
||||||
# S112 — `except: continue`, the silent swallow inside a loop
|
# S112 — `except: continue`, the silent swallow inside a loop
|
||||||
@@ -45,7 +47,6 @@ target-version = "py312"
|
|||||||
extend-select = [
|
extend-select = [
|
||||||
"LOG", "G", "T20", "TRY400", "TRY401", "S110", "S112", "BLE001", "RUF100", "PLW1510", "B",
|
"LOG", "G", "T20", "TRY400", "TRY401", "S110", "S112", "BLE001", "RUF100", "PLW1510", "B",
|
||||||
]
|
]
|
||||||
ignore = ["E402"]
|
|
||||||
|
|
||||||
[lint.flake8-bugbear]
|
[lint.flake8-bugbear]
|
||||||
extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"]
|
extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"]
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ hold for Google Workspace custom domains unless their admin enabled it.
|
|||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy import select, text
|
from sqlalchemy import select, text
|
||||||
|
|
||||||
_UAT_PW = "correct horse battery"
|
|
||||||
|
|
||||||
from app.auth import is_allowed
|
from app.auth import is_allowed
|
||||||
from app.models import Invite, User
|
from app.models import Invite, User
|
||||||
from app.utils import canonical_email
|
from app.utils import canonical_email
|
||||||
|
|
||||||
|
_UAT_PW = "correct horse battery"
|
||||||
|
|
||||||
# (input, expected canonical) — the matrix the sprint promised, plus the shapes that must NOT fold.
|
# (input, expected canonical) — the matrix the sprint promised, plus the shapes that must NOT fold.
|
||||||
CASES = [
|
CASES = [
|
||||||
# Gmail: dots and +tags are noise…
|
# Gmail: dots and +tags are noise…
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ project checks needs a row.
|
|||||||
|
|
||||||
| Scope | Narrowed | Why | Measured |
|
| Scope | Narrowed | Why | Measured |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| backend + scripts | `E402` — ignored | A few modules (`main.py`, `scripts/e2e_seed.py`) run setup — logging, `sys.path` — before their imports on purpose. | — |
|
|
||||||
| backend + scripts | `B008` — narrowed: `fastapi.Depends`, `fastapi.Query` declared immutable | FastAPI's dependency injection IS a call in a default argument, on every route. Every other call in a default still fails. | 474 sites, all of them FastAPI |
|
| backend + scripts | `B008` — narrowed: `fastapi.Depends`, `fastapi.Query` declared immutable | FastAPI's dependency injection IS a call in a default argument, on every route. Every other call in a default still fails. | 474 sites, all of them FastAPI |
|
||||||
| backend/app | `C901` — not in ruff; measured by `tests/test_source_size.py` | Ruff has no per-function baseline, so turning it on would need a `noqa` per offender, and a `noqa` lets a function grow without limit. The test holds each offender, by name, at its current value. Tests and scripts are outside it: see the size / complexity rows under Lanes. | 32 functions over 10 |
|
| backend/app | `C901` — not in ruff; measured by `tests/test_source_size.py` | Ruff has no per-function baseline, so turning it on would need a `noqa` per offender, and a `noqa` lets a function grow without limit. The test holds each offender, by name, at its current value. Tests and scripts are outside it: see the size / complexity rows under Lanes. | 32 functions over 10 |
|
||||||
|
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ describe("the gate register (docs/gates.md)", () => {
|
|||||||
return m ? [...m[1]!.matchAll(/"([^"]+)"/g)].map((x) => x[1]!) : [];
|
return m ? [...m[1]!.matchAll(/"([^"]+)"/g)].map((x) => x[1]!) : [];
|
||||||
};
|
};
|
||||||
expect(tomlKeys(toml).sort()).toEqual(
|
expect(tomlKeys(toml).sort()).toEqual(
|
||||||
["extend-immutable-calls", "extend-select", "ignore", "line-length", "target-version"].sort(),
|
["extend-immutable-calls", "extend-select", "line-length", "target-version"].sort(),
|
||||||
);
|
);
|
||||||
// the selection on top of ruff's defaults (E, F) — dropping one narrows the lane
|
// the selection on top of ruff's defaults (E, F) — dropping one narrows the lane
|
||||||
expect(list("extend-select")).toEqual([
|
expect(list("extend-select")).toEqual([
|
||||||
|
|||||||
Reference in New Issue
Block a user