Skip to content

DM-55493: scope PLR0917 to exclude FastAPI handlers - #351

Closed
jonathansick wants to merge 1 commit into
mainfrom
tickets/DM-55493-ruff-lint
Closed

DM-55493: scope PLR0917 to exclude FastAPI handlers#351
jonathansick wants to merge 1 commit into
mainfrom
tickets/DM-55493-ruff-lint

Conversation

@jonathansick

Copy link
Copy Markdown
Member

Motivation

ruff 0.16.0 (2026-07-23) stabilized PLR0917 (too-many-positional-arguments) out of preview. Because ruff-shared.toml uses select = ["ALL"], the rule armed automatically in every repo that picked up the new ruff — no config change required.

PLR0917 is a good rule for ordinary internal APIs: a call site passing six bare positional arguments really is hard to read, and the fix (make some of them keyword-only) really is an improvement.

It is not a good rule for FastAPI route handlers. A handler routinely exceeds five parameters — path parameters, query parameters, and one or more Depends(...) injections — and that signature is not a call-site ergonomics problem, because our code never calls handlers at all. FastAPI calls them, by keyword, from the resolved dependency graph. So in handlers/ the rule protects nothing and is pure noise.

Change

Adds PLR0917 to the per-file-ignore entries that already scope rules to the handlers package, in the fastapi_safir_app templates only:

"src/*/handlers/**" = [
    "D103",    # FastAPI handlers should not have docstrings
    "PLR0917", # FastAPI injects handler arguments by keyword, not position
]

square_pypi_package is deliberately untouched — it has no handlers/.

Why a scoped ignore and not a global one

ruff-shared.toml already globally ignores the sibling rule PLR0913 (too-many-arguments), with the comment "factory pattern uses constructors with many arguments". It would be easy to assume PLR0917 belongs next to it. It doesn't — they are different concerns:

  • PLR0913 counts all arguments. Our factory/constructor pattern legitimately has many, everywhere in the codebase, so a global ignore is the right call.
  • PLR0917 counts only positional arguments — i.e. it is a complaint about call sites, and it is silenced by making parameters keyword-only. That is usually the right fix, so the rule should keep firing on ordinary internal APIs and drive better signatures there.

FastAPI handlers are the narrow case where the fix is neither available nor wanted (the signature is an interface contract with FastAPI, not with us). Hence a path-scoped exemption rather than a blanket entry in ignore.

Glob choice

I followed the file's existing convention rather than introducing a new "**/handlers/**" key: the config already carries the handlers path as a pair of globs, "src/*/handlers/**" and "*/src/*/handlers/**", mirroring the same pairing used for tests/** and */tests/**. PLR0917 was added to both so the entries stay symmetric. Happy to switch to a single **/handlers/** if maintainers would rather consolidate — but that felt like a separate cleanup touching several unrelated entries.

Validation

  • Applied to all three copies ({{cookiecutter.name}}, example, example-uws); the three files remain byte-identical (same md5).

  • templatekit check (the CI job — re-renders every example with scons and diffs against the committed state) passes: ✅ Passed!

  • All five ruff-shared.toml files parse as TOML; confirmed PLR0917 is not present in any global ignore list.

  • Behavior verified against ruff 0.16.0 specifically (the repo's pinned pre-commit ruff is older and does not know these rules), using a scratch project that extends this config and contains the same 7-positional-argument function in two places:

    config src/myapp/handlers/external.py src/myapp/services/thing.py
    as on main PLR0917 fires PLR0917 fires
    with this patch clean PLR0917 still fires (intended)
  • uvx ruff@0.16.0 check --select PLR0917,ISC004 . inside the rendered example/ app: All checks passed!

    Two honest caveats on that command:

    • It cannot be run at the repo root — ruff fails to parse {{cookiecutter.name}}/pyproject.toml, which contains Jinja. That is a property of a cookiecutter repo, not of this change.
    • In example-uws/ it reports a pre-existing invalid-syntax in src/exampleuws/dependencies.py:15 (a scaffold stub: a bare *, separator followed only by comments). That file is byte-identical to main and is untouched here.

No changelog fragment: this repo has no changelog.d/ at its root, and a lint-config adjustment is not user-visible in the way scriv fragments track.

Ticket: DM-55493

ruff 0.16.0 stabilized PLR0917 (too-many-positional-arguments) out of
preview. Since ruff-shared.toml selects ALL, it now fires on every
FastAPI route handler, which legitimately takes more than five
parameters (path and query parameters plus Depends injections) and is
only ever called by FastAPI itself, by keyword. Add PLR0917 to the
existing handlers per-file-ignores rather than the global ignore list,
so the rule keeps firing on ordinary internal APIs.
@jonathansick
jonathansick requested a review from rra July 27, 2026 20:18

@rra rra left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I like this rule and would rather not exclude it. I want to leave it active in the shared Ruff configuration so that it affects my packages.

FastAPI route handlers are regular functions and sometimes it's useful to call them from, e.g., another route handler. In those cases, long argument lists should be passed by keyword for all the reasons that we would do so elsewhere. I think it's fairly easy to just add *, to the start of the argument list for FastAPI route handlers, which satisfies this diagnostic.

@jonathansick

Copy link
Copy Markdown
Member Author

Ok good call. I'll adapt the signatures on my handlers.

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.

2 participants