[`ruff`] Implement `RUF102` (`fastapi-redundant-response-model`)
Summary
Implements ruff specific role for fastapi routes, and its autofix.
Test Plan
cargo test / cargo insta review
ruff-ecosystem results
Linter (stable)
✅ ecosystem check detected no linter changes.
Linter (preview)
ℹ️ ecosystem check detected linter changes. (+3 -0 violations, +0 -0 fixes in 1 projects; 49 projects unchanged)
lnbits/lnbits (+3 -0 violations, +0 -0 fixes)
ruff check --no-cache --exit-zero --ignore RUF9 --output-format concise --preview
+ lnbits/core/views/api.py:66:37: RUF102 FastAPI route with redundant response_model argument + lnbits/core/views/node_api.py:79:34: RUF102 FastAPI route with redundant response_model argument + lnbits/core/views/wallet_api.py:72:25: RUF102 FastAPI route with redundant response_model argument
Changes by rule (1 rules affected)
| code | total | + violation | - violation | + fix | - fix |
|---|---|---|---|---|---|
| RUF102 | 3 | 3 | 0 | 0 | 0 |
CodSpeed Performance Report
Merging #11579 will not alter performance
Comparing TomerBin:fastapi-redundant-response-model (dd50d3b) with TomerBin:fastapi-redundant-response-model (13df570)
Summary
✅ 33 untouched benchmarks
Thank you for this! Two reactions, which I'd in-turn like your reaction on:
- I think we'll need to find someone who feels comfortable reviewing these rules for correctness and quality (e.g., is this a recommended pattern?). I'm not familiar enough with FastAPI to be a good judge here. Perhaps I can ask Sebastian what he thinks of it.
- Ideally we'd implement multiple FastAPI rules before committing to supporting a one-off rule. This helps prevent Ruff from feeling like an unwieldy collection of one-off rules.
@charliermarsh Thanks for your comments.
- It would be great if Sebastian could confirm that rule. Could you please ask for his opinion?
- Another rule I'm considering is to use the new Annotated style for dependencies declaration. Other rules I have in mind require complicated type inference and cross-file analysis, so they are probably out of scope for this PR. Do you think multiple rules are mandatory for merging this PR?
@charliermarsh Any chance you could provide feedback on my previous message when you have a moment?
Thanks @TomerBin for emailing me! This looks great.
And recommending Annotated would be awesome as well.
Additionally, if there was a way to auto-fix code that uses the old style to now use Annotated in Ruff, that would be a dream come true. It applies to dependencies or to Security as well (another type of dependency.
An old dependency and Security param would look like:
@app.get("/items/")
def get_items(
current_user: User = Depends(get_current_user),
some_security_param: str = Security(get_oauth2_user),
):
# do stuff
pass
That would be ideally transformed into:
@app.get("/items/")
def get_items(
current_user: Annotated[User, Depends(get_current_user)],
some_security_param: Annotated[str, Security(get_oauth2_user)],
):
# do stuff
pass
Additionally, the same transformation would apply to other types of parameters, like Query, Path, Body, Cookie.
The only caveat is that if one of those has a default argument (or the first argument), as in Query(default="foo") then that would become the new default value. For example:
@app.post("/stuff/")
def do_stuff(
some_query_param: str | None = Query(default=None),
some_path_param: str = Path(),
some_body_param: str = Body("foo"),
some_cookie_param: str = Cookie(),
some_header_param: int = Header(default=5),
some_file_param: UploadFile = File(),
some_form_param: str = Form(),
):
# do stuff
pass
would ideally be transformed into:
@app.post("/stuff/")
def do_stuff(
some_query_param: Annotated[str | None, Query()] = None,
some_path_param: Annotated[str, Path()],
some_body_param: Annotated[str, Body()] = "foo",
some_cookie_param: Annotated[str, Cookie()],
some_header_param: Annotated[int, Header()] = 5,
some_file_param: Annotated[UploadFile, File()],
some_form_param: Annotated[str, Form()],
):
# do stuff
pass
Note: please keep pinging me on email if I miss a GitHub notification around this, I miss most GitHub notifications as I still have around 10k unread. :sweat_smile:
Thank you @TomerBin and @tiangolo 🙏
@TomerBin, thanks for your work on this!
Is this rule intended to apply only to route functions? I ask because at my company, we have a pretty comprehensive set of free functions that are used as dependency providers, for example:
controller.py:
@app.post("/")
def route(dependency: Annotated[str, Depends(providers.base_provider)]) -> None:
...
providers.py:
def base_provider(sub_provider: Annotated[str, Depends(sub_provder)]) -> str:
...
def sub_provider() -> str:
...
Currently, this rule only detects and fixes the route function, and not the provider function in providers.py (which would be pretty nice). As far as I can tell, this wouldn't be susceptible to false positives, since FastAPI dependencies are marked with a FastAPI-specific class.