Skip to content

Commit

Permalink
[fastapi] Implement FAST001 (fastapi-redundant-response-model) …
Browse files Browse the repository at this point in the history
…and `FAST002` (`fastapi-non-annotated-dependency`) (#11579)

## Summary

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

## Test Plan

`cargo test` / `cargo insta review`
  • Loading branch information
TomerBin authored Jul 21, 2024
1 parent 8235571 commit 0532436
Show file tree
Hide file tree
Showing 14 changed files with 1,009 additions and 5 deletions.
110 changes: 110 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/fastapi/FAST001.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from typing import List, Dict

from fastapi import FastAPI, APIRouter
from pydantic import BaseModel

app = FastAPI()
router = APIRouter()


class Item(BaseModel):
name: str


# Errors


@app.post("/items/", response_model=Item)
async def create_item(item: Item) -> Item:
return item


@app.post("/items/", response_model=list[Item])
async def create_item(item: Item) -> list[Item]:
return item


@app.post("/items/", response_model=List[Item])
async def create_item(item: Item) -> List[Item]:
return item


@app.post("/items/", response_model=Dict[str, Item])
async def create_item(item: Item) -> Dict[str, Item]:
return item


@app.post("/items/", response_model=str)
async def create_item(item: Item) -> str:
return item


@app.get("/items/", response_model=Item)
async def create_item(item: Item) -> Item:
return item


@app.get("/items/", response_model=Item)
@app.post("/items/", response_model=Item)
async def create_item(item: Item) -> Item:
return item


@router.get("/items/", response_model=Item)
async def create_item(item: Item) -> Item:
return item


# OK


async def create_item(item: Item) -> Item:
return item


@app("/items/", response_model=Item)
async def create_item(item: Item) -> Item:
return item


@cache
async def create_item(item: Item) -> Item:
return item


@app.post("/items/", response_model=str)
async def create_item(item: Item) -> Item:
return item


@app.post("/items/")
async def create_item(item: Item) -> Item:
return item


@app.post("/items/", response_model=str)
async def create_item(item: Item):
return item


@app.post("/items/", response_model=list[str])
async def create_item(item: Item) -> Dict[str, Item]:
return item


@app.post("/items/", response_model=list[str])
async def create_item(item: Item) -> list[str, str]:
return item


@app.post("/items/", response_model=Dict[str, int])
async def create_item(item: Item) -> Dict[str, str]:
return item


app = None


@app.post("/items/", response_model=Item)
async def create_item(item: Item) -> Item:
return item
68 changes: 68 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/fastapi/FAST002.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from fastapi import (
FastAPI,
APIRouter,
Query,
Path,
Body,
Cookie,
Header,
File,
Form,
Depends,
Security,
)
from pydantic import BaseModel

app = FastAPI()
router = APIRouter()


# Errors

@app.get("/items/")
def get_items(
current_user: User = Depends(get_current_user),
some_security_param: str = Security(get_oauth2_user),
):
pass


@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


# Unchanged


@app.post("/stuff/")
def do_stuff(
no_default: Body("foo"),
no_type_annotation=str,
no_fastapi_default: str = BaseModel(),
):
pass


# OK

@app.post("/stuff/")
def do_stuff(
some_path_param: Annotated[str, Path()],
some_cookie_param: Annotated[str, Cookie()],
some_file_param: Annotated[UploadFile, File()],
some_form_param: Annotated[str, Form()],
some_query_param: Annotated[str | None, Query()] = None,
some_body_param: Annotated[str, Body()] = "foo",
some_header_param: Annotated[int, Header()] = 5,
):
pass
16 changes: 11 additions & 5 deletions crates/ruff_linter/src/checkers/ast/analyze/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::registry::Rule;
use crate::rules::{
airflow, flake8_async, flake8_bandit, flake8_boolean_trap, flake8_bugbear, flake8_builtins,
flake8_debugger, flake8_django, flake8_errmsg, flake8_import_conventions, flake8_pie,
flake8_pyi, flake8_pytest_style, flake8_raise, flake8_return, flake8_simplify, flake8_slots,
flake8_tidy_imports, flake8_type_checking, mccabe, pandas_vet, pep8_naming, perflint,
pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade, refurb, ruff, tryceratops,
airflow, fastapi, flake8_async, flake8_bandit, flake8_boolean_trap, flake8_bugbear,
flake8_builtins, flake8_debugger, flake8_django, flake8_errmsg, flake8_import_conventions,
flake8_pie, flake8_pyi, flake8_pytest_style, flake8_raise, flake8_return, flake8_simplify,
flake8_slots, flake8_tidy_imports, flake8_type_checking, mccabe, pandas_vet, pep8_naming,
perflint, pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade, refurb, ruff, tryceratops,
};
use crate::settings::types::PythonVersion;

Expand Down Expand Up @@ -88,6 +88,12 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::DjangoNonLeadingReceiverDecorator) {
flake8_django::rules::non_leading_receiver_decorator(checker, decorator_list);
}
if checker.enabled(Rule::FastApiRedundantResponseModel) {
fastapi::rules::fastapi_redundant_response_model(checker, function_def);
}
if checker.enabled(Rule::FastApiNonAnnotatedDependency) {
fastapi::rules::fastapi_non_annotated_dependency(checker, function_def);
}
if checker.enabled(Rule::AmbiguousFunctionName) {
if let Some(diagnostic) = pycodestyle::rules::ambiguous_function_name(name) {
checker.diagnostics.push(diagnostic);
Expand Down
5 changes: 5 additions & 0 deletions crates/ruff_linter/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Numpy, "003") => (RuleGroup::Stable, rules::numpy::rules::NumpyDeprecatedFunction),
(Numpy, "201") => (RuleGroup::Stable, rules::numpy::rules::Numpy2Deprecation),

// fastapi
(FastApi, "001") => (RuleGroup::Preview, rules::fastapi::rules::FastApiRedundantResponseModel),
(FastApi, "002") => (RuleGroup::Preview, rules::fastapi::rules::FastApiNonAnnotatedDependency),

// pydoclint
(Pydoclint, "501") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingException),
(Pydoclint, "502") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringExtraneousException),
Expand Down Expand Up @@ -947,6 +951,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "030") => (RuleGroup::Preview, rules::ruff::rules::AssertWithPrintMessage),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "101") => (RuleGroup::Preview, rules::ruff::rules::RedirectedNOQA),

(Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml),
#[cfg(any(feature = "test-rules", test))]
(Ruff, "900") => (RuleGroup::Stable, rules::ruff::rules::StableTestRule),
Expand Down
3 changes: 3 additions & 0 deletions crates/ruff_linter/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ pub enum Linter {
/// NumPy-specific rules
#[prefix = "NPY"]
Numpy,
/// [FastAPI](https://pypi.org/project/fastapi/)
#[prefix = "FAST"]
FastApi,
/// [Airflow](https://pypi.org/project/apache-airflow/)
#[prefix = "AIR"]
Airflow,
Expand Down
27 changes: 27 additions & 0 deletions crates/ruff_linter/src/rules/fastapi/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//! FastAPI-specific rules.
pub(crate) mod rules;

#[cfg(test)]
mod tests {
use std::convert::AsRef;
use std::path::Path;

use anyhow::Result;
use test_case::test_case;

use crate::registry::Rule;
use crate::test::test_path;
use crate::{assert_messages, settings};

#[test_case(Rule::FastApiRedundantResponseModel, Path::new("FAST001.py"))]
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("fastapi").join(path).as_path(),
&settings::LinterSettings::for_rule(rule_code),
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
}
Loading

0 comments on commit 0532436

Please sign in to comment.