ruff icon indicating copy to clipboard operation
ruff copied to clipboard

[`ruff`] Implement `RUF102` (`fastapi-redundant-response-model`)

Open TomerBin opened this issue 1 year ago • 5 comments

Summary

Implements ruff specific role for fastapi routes, and its autofix.

Test Plan

cargo test / cargo insta review

TomerBin avatar May 28 '24 07:05 TomerBin

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

github-actions[bot] avatar May 28 '24 07:05 github-actions[bot]

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

codspeed-hq[bot] avatar May 28 '24 07:05 codspeed-hq[bot]

Thank you for this! Two reactions, which I'd in-turn like your reaction on:

  1. 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.
  2. 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 avatar Jun 04 '24 04:06 charliermarsh

@charliermarsh Thanks for your comments.

  1. It would be great if Sebastian could confirm that rule. Could you please ask for his opinion?
  2. 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?

TomerBin avatar Jun 15 '24 11:06 TomerBin

@charliermarsh Any chance you could provide feedback on my previous message when you have a moment?

TomerBin avatar Jul 06 '24 16:07 TomerBin

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:

tiangolo avatar Jul 15 '24 23:07 tiangolo

Thank you @TomerBin and @tiangolo 🙏

charliermarsh avatar Jul 16 '24 00:07 charliermarsh

@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.

marcuslimdw avatar Jul 30 '24 04:07 marcuslimdw