[fastapi] Implement FAST001 (fastapi-redundant-response-model) 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`
This commit is contained in:
TomerBin 2024-07-21 21:28:10 +03:00 committed by GitHub
parent 82355712c3
commit 053243635c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1009 additions and 5 deletions

View file

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

View file

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

View file

@ -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;
@ -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);

View file

@ -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),
@ -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),

View file

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

View file

@ -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(())
}
}

View file

@ -0,0 +1,138 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::helpers::map_callable;
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::rules::fastapi::rules::is_fastapi_route;
use crate::settings::types::PythonVersion;
/// ## What it does
/// Identifies FastAPI routes with deprecated uses of `Depends`.
///
/// ## Why is this bad?
/// The FastAPI documentation recommends the use of `Annotated` for defining
/// route dependencies and parameters, rather than using `Depends` directly
/// with a default value.
///
/// This approach is also suggested for various route parameters, including Body and Cookie, as it helps ensure consistency and clarity in defining dependencies and parameters.
///
/// ## Example
///
/// ```python
/// from fastapi import Depends, FastAPI
///
/// app = FastAPI()
///
///
/// async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
/// return {"q": q, "skip": skip, "limit": limit}
///
///
/// @app.get("/items/")
/// async def read_items(commons: dict = Depends(common_parameters)):
/// return commons
/// ```
///
/// Use instead:
///
/// ```python
/// from typing import Annotated
///
/// from fastapi import Depends, FastAPI
///
/// app = FastAPI()
///
///
/// async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
/// return {"q": q, "skip": skip, "limit": limit}
///
///
/// @app.get("/items/")
/// async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
/// return commons
/// ```
#[violation]
pub struct FastApiNonAnnotatedDependency;
impl AlwaysFixableViolation for FastApiNonAnnotatedDependency {
#[derive_message_formats]
fn message(&self) -> String {
format!("FastAPI dependency without `Annotated`")
}
fn fix_title(&self) -> String {
"Replace with `Annotated`".to_string()
}
}
/// RUF103
pub(crate) fn fastapi_non_annotated_dependency(
checker: &mut Checker,
function_def: &ast::StmtFunctionDef,
) {
if !checker.semantic().seen_module(Modules::FASTAPI) {
return;
}
if !is_fastapi_route(function_def, checker.semantic()) {
return;
}
for parameter in &function_def.parameters.args {
if let (Some(annotation), Some(default)) =
(&parameter.parameter.annotation, &parameter.default)
{
if checker
.semantic()
.resolve_qualified_name(map_callable(default))
.is_some_and(|qualified_name| {
matches!(
qualified_name.segments(),
[
"fastapi",
"Query"
| "Path"
| "Body"
| "Cookie"
| "Header"
| "File"
| "Form"
| "Depends"
| "Security"
]
)
})
{
let mut diagnostic =
Diagnostic::new(FastApiNonAnnotatedDependency, parameter.range);
diagnostic.try_set_fix(|| {
let module = if checker.settings.target_version >= PythonVersion::Py39 {
"typing"
} else {
"typing_extensions"
};
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import_from(module, "Annotated"),
function_def.start(),
checker.semantic(),
)?;
let content = format!(
"{}: {}[{}, {}]",
parameter.parameter.name.id,
binding,
checker.locator().slice(annotation.range()),
checker.locator().slice(default.range())
);
let parameter_edit = Edit::range_replacement(content, parameter.range());
Ok(Fix::unsafe_edits(import_edit, [parameter_edit]))
});
checker.diagnostics.push(diagnostic);
}
}
}
}

View file

@ -0,0 +1,158 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Decorator, Expr, ExprCall, Keyword, StmtFunctionDef};
use ruff_python_semantic::{Modules, SemanticModel};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::edits::{remove_argument, Parentheses};
use crate::rules::fastapi::rules::is_fastapi_route_decorator;
/// ## What it does
/// Checks for FastAPI routes that use the optional `response_model` parameter
/// with the same type as the return type.
///
/// ## Why is this bad?
/// FastAPI routes automatically infer the response model type from the return
/// type, so specifying it explicitly is redundant.
///
/// The `response_model` parameter is used to override the default response
/// model type. For example, `response_model` can be used to specify that
/// a non-serializable response type should instead be serialized via an
/// alternative type.
///
/// For more information, see the [FastAPI documentation](https://fastapi.tiangolo.com/tutorial/response-model/).
///
/// ## Example
///
/// ```python
/// from fastapi import FastAPI
/// from pydantic import BaseModel
///
/// app = FastAPI()
///
///
/// class Item(BaseModel):
/// name: str
///
///
/// @app.post("/items/", response_model=Item)
/// async def create_item(item: Item) -> Item:
/// return item
/// ```
///
/// Use instead:
///
/// ```python
/// from fastapi import FastAPI
/// from pydantic import BaseModel
///
/// app = FastAPI()
///
///
/// class Item(BaseModel):
/// name: str
///
///
/// @app.post("/items/")
/// async def create_item(item: Item) -> Item:
/// return item
/// ```
#[violation]
pub struct FastApiRedundantResponseModel;
impl AlwaysFixableViolation for FastApiRedundantResponseModel {
#[derive_message_formats]
fn message(&self) -> String {
format!("FastAPI route with redundant `response_model` argument")
}
fn fix_title(&self) -> String {
"Remove argument".to_string()
}
}
/// RUF102
pub(crate) fn fastapi_redundant_response_model(
checker: &mut Checker,
function_def: &StmtFunctionDef,
) {
if !checker.semantic().seen_module(Modules::FASTAPI) {
return;
}
for decorator in &function_def.decorator_list {
let Some((call, response_model_arg)) =
check_decorator(function_def, decorator, checker.semantic())
else {
continue;
};
let mut diagnostic =
Diagnostic::new(FastApiRedundantResponseModel, response_model_arg.range());
diagnostic.try_set_fix(|| {
remove_argument(
response_model_arg,
&call.arguments,
Parentheses::Preserve,
checker.locator().contents(),
)
.map(Fix::unsafe_edit)
});
checker.diagnostics.push(diagnostic);
}
}
fn check_decorator<'a>(
function_def: &StmtFunctionDef,
decorator: &'a Decorator,
semantic: &'a SemanticModel,
) -> Option<(&'a ExprCall, &'a Keyword)> {
let call = is_fastapi_route_decorator(decorator, semantic)?;
let response_model_arg = call.arguments.find_keyword("response_model")?;
let return_value = function_def.returns.as_ref()?;
if is_identical_types(&response_model_arg.value, return_value, semantic) {
Some((call, response_model_arg))
} else {
None
}
}
fn is_identical_types(
response_model_arg: &Expr,
return_value: &Expr,
semantic: &SemanticModel,
) -> bool {
if let (Some(response_mode_name_expr), Some(return_value_name_expr)) = (
response_model_arg.as_name_expr(),
return_value.as_name_expr(),
) {
return semantic.resolve_name(response_mode_name_expr)
== semantic.resolve_name(return_value_name_expr);
}
if let (Some(response_mode_subscript), Some(return_value_subscript)) = (
response_model_arg.as_subscript_expr(),
return_value.as_subscript_expr(),
) {
return is_identical_types(
&response_mode_subscript.value,
&return_value_subscript.value,
semantic,
) && is_identical_types(
&response_mode_subscript.slice,
&return_value_subscript.slice,
semantic,
);
}
if let (Some(response_mode_tuple), Some(return_value_tuple)) = (
response_model_arg.as_tuple_expr(),
return_value.as_tuple_expr(),
) {
return response_mode_tuple.elts.len() == return_value_tuple.elts.len()
&& response_mode_tuple
.elts
.iter()
.zip(return_value_tuple.elts.iter())
.all(|(x, y)| is_identical_types(x, y, semantic));
}
false
}

View file

@ -0,0 +1,44 @@
pub(crate) use fastapi_non_annotated_dependency::*;
pub(crate) use fastapi_redundant_response_model::*;
mod fastapi_non_annotated_dependency;
mod fastapi_redundant_response_model;
use ruff_python_ast::{Decorator, ExprCall, StmtFunctionDef};
use ruff_python_semantic::analyze::typing::resolve_assignment;
use ruff_python_semantic::SemanticModel;
/// Returns `true` if the function is a FastAPI route.
pub(crate) fn is_fastapi_route(function_def: &StmtFunctionDef, semantic: &SemanticModel) -> bool {
return function_def
.decorator_list
.iter()
.any(|decorator| is_fastapi_route_decorator(decorator, semantic).is_some());
}
/// Returns `true` if the decorator is indicative of a FastAPI route.
pub(crate) fn is_fastapi_route_decorator<'a>(
decorator: &'a Decorator,
semantic: &'a SemanticModel,
) -> Option<&'a ExprCall> {
let call = decorator.expression.as_call_expr()?;
let decorator_method = call.func.as_attribute_expr()?;
let method_name = &decorator_method.attr;
if !matches!(
method_name.as_str(),
"get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "trace"
) {
return None;
}
let qualified_name = resolve_assignment(&decorator_method.value, semantic)?;
if matches!(
qualified_name.segments(),
["fastapi", "FastAPI" | "APIRouter"]
) {
Some(call)
} else {
None
}
}

View file

@ -0,0 +1,263 @@
---
source: crates/ruff_linter/src/rules/fastapi/mod.rs
---
FAST002.py:24:5: FAST002 [*] FastAPI dependency without `Annotated`
|
22 | @app.get("/items/")
23 | def get_items(
24 | current_user: User = Depends(get_current_user),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
25 | some_security_param: str = Security(get_oauth2_user),
26 | ):
|
= help: Replace with `Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
21 22 |
22 23 | @app.get("/items/")
23 24 | def get_items(
24 |- current_user: User = Depends(get_current_user),
25 |+ current_user: Annotated[User, Depends(get_current_user)],
25 26 | some_security_param: str = Security(get_oauth2_user),
26 27 | ):
27 28 | pass
FAST002.py:25:5: FAST002 [*] FastAPI dependency without `Annotated`
|
23 | def get_items(
24 | current_user: User = Depends(get_current_user),
25 | some_security_param: str = Security(get_oauth2_user),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
26 | ):
27 | pass
|
= help: Replace with `Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
22 23 | @app.get("/items/")
23 24 | def get_items(
24 25 | current_user: User = Depends(get_current_user),
25 |- some_security_param: str = Security(get_oauth2_user),
26 |+ some_security_param: Annotated[str, Security(get_oauth2_user)],
26 27 | ):
27 28 | pass
28 29 |
FAST002.py:32:5: FAST002 [*] FastAPI dependency without `Annotated`
|
30 | @app.post("/stuff/")
31 | def do_stuff(
32 | some_query_param: str | None = Query(default=None),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
33 | some_path_param: str = Path(),
34 | some_body_param: str = Body("foo"),
|
= help: Replace with `Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
29 30 |
30 31 | @app.post("/stuff/")
31 32 | def do_stuff(
32 |- some_query_param: str | None = Query(default=None),
33 |+ some_query_param: Annotated[str | None, Query(default=None)],
33 34 | some_path_param: str = Path(),
34 35 | some_body_param: str = Body("foo"),
35 36 | some_cookie_param: str = Cookie(),
FAST002.py:33:5: FAST002 [*] FastAPI dependency without `Annotated`
|
31 | def do_stuff(
32 | some_query_param: str | None = Query(default=None),
33 | some_path_param: str = Path(),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
34 | some_body_param: str = Body("foo"),
35 | some_cookie_param: str = Cookie(),
|
= help: Replace with `Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
30 31 | @app.post("/stuff/")
31 32 | def do_stuff(
32 33 | some_query_param: str | None = Query(default=None),
33 |- some_path_param: str = Path(),
34 |+ some_path_param: Annotated[str, Path()],
34 35 | some_body_param: str = Body("foo"),
35 36 | some_cookie_param: str = Cookie(),
36 37 | some_header_param: int = Header(default=5),
FAST002.py:34:5: FAST002 [*] FastAPI dependency without `Annotated`
|
32 | some_query_param: str | None = Query(default=None),
33 | some_path_param: str = Path(),
34 | some_body_param: str = Body("foo"),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
35 | some_cookie_param: str = Cookie(),
36 | some_header_param: int = Header(default=5),
|
= help: Replace with `Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
31 32 | def do_stuff(
32 33 | some_query_param: str | None = Query(default=None),
33 34 | some_path_param: str = Path(),
34 |- some_body_param: str = Body("foo"),
35 |+ some_body_param: Annotated[str, Body("foo")],
35 36 | some_cookie_param: str = Cookie(),
36 37 | some_header_param: int = Header(default=5),
37 38 | some_file_param: UploadFile = File(),
FAST002.py:35:5: FAST002 [*] FastAPI dependency without `Annotated`
|
33 | some_path_param: str = Path(),
34 | some_body_param: str = Body("foo"),
35 | some_cookie_param: str = Cookie(),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
36 | some_header_param: int = Header(default=5),
37 | some_file_param: UploadFile = File(),
|
= help: Replace with `Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
32 33 | some_query_param: str | None = Query(default=None),
33 34 | some_path_param: str = Path(),
34 35 | some_body_param: str = Body("foo"),
35 |- some_cookie_param: str = Cookie(),
36 |+ some_cookie_param: Annotated[str, Cookie()],
36 37 | some_header_param: int = Header(default=5),
37 38 | some_file_param: UploadFile = File(),
38 39 | some_form_param: str = Form(),
FAST002.py:36:5: FAST002 [*] FastAPI dependency without `Annotated`
|
34 | some_body_param: str = Body("foo"),
35 | some_cookie_param: str = Cookie(),
36 | some_header_param: int = Header(default=5),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
37 | some_file_param: UploadFile = File(),
38 | some_form_param: str = Form(),
|
= help: Replace with `Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
33 34 | some_path_param: str = Path(),
34 35 | some_body_param: str = Body("foo"),
35 36 | some_cookie_param: str = Cookie(),
36 |- some_header_param: int = Header(default=5),
37 |+ some_header_param: Annotated[int, Header(default=5)],
37 38 | some_file_param: UploadFile = File(),
38 39 | some_form_param: str = Form(),
39 40 | ):
FAST002.py:37:5: FAST002 [*] FastAPI dependency without `Annotated`
|
35 | some_cookie_param: str = Cookie(),
36 | some_header_param: int = Header(default=5),
37 | some_file_param: UploadFile = File(),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
38 | some_form_param: str = Form(),
39 | ):
|
= help: Replace with `Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
34 35 | some_body_param: str = Body("foo"),
35 36 | some_cookie_param: str = Cookie(),
36 37 | some_header_param: int = Header(default=5),
37 |- some_file_param: UploadFile = File(),
38 |+ some_file_param: Annotated[UploadFile, File()],
38 39 | some_form_param: str = Form(),
39 40 | ):
40 41 | # do stuff
FAST002.py:38:5: FAST002 [*] FastAPI dependency without `Annotated`
|
36 | some_header_param: int = Header(default=5),
37 | some_file_param: UploadFile = File(),
38 | some_form_param: str = Form(),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
39 | ):
40 | # do stuff
|
= help: Replace with `Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
35 36 | some_cookie_param: str = Cookie(),
36 37 | some_header_param: int = Header(default=5),
37 38 | some_file_param: UploadFile = File(),
38 |- some_form_param: str = Form(),
39 |+ some_form_param: Annotated[str, Form()],
39 40 | ):
40 41 | # do stuff
41 42 | pass

View file

@ -0,0 +1,174 @@
---
source: crates/ruff_linter/src/rules/fastapi/mod.rs
---
FAST001.py:17:22: FAST001 [*] FastAPI route with redundant `response_model` argument
|
17 | @app.post("/items/", response_model=Item)
| ^^^^^^^^^^^^^^^^^^^ FAST001
18 | async def create_item(item: Item) -> Item:
19 | return item
|
= help: Remove argument
Unsafe fix
14 14 | # Errors
15 15 |
16 16 |
17 |-@app.post("/items/", response_model=Item)
17 |+@app.post("/items/")
18 18 | async def create_item(item: Item) -> Item:
19 19 | return item
20 20 |
FAST001.py:22:22: FAST001 [*] FastAPI route with redundant `response_model` argument
|
22 | @app.post("/items/", response_model=list[Item])
| ^^^^^^^^^^^^^^^^^^^^^^^^^ FAST001
23 | async def create_item(item: Item) -> list[Item]:
24 | return item
|
= help: Remove argument
Unsafe fix
19 19 | return item
20 20 |
21 21 |
22 |-@app.post("/items/", response_model=list[Item])
22 |+@app.post("/items/")
23 23 | async def create_item(item: Item) -> list[Item]:
24 24 | return item
25 25 |
FAST001.py:27:22: FAST001 [*] FastAPI route with redundant `response_model` argument
|
27 | @app.post("/items/", response_model=List[Item])
| ^^^^^^^^^^^^^^^^^^^^^^^^^ FAST001
28 | async def create_item(item: Item) -> List[Item]:
29 | return item
|
= help: Remove argument
Unsafe fix
24 24 | return item
25 25 |
26 26 |
27 |-@app.post("/items/", response_model=List[Item])
27 |+@app.post("/items/")
28 28 | async def create_item(item: Item) -> List[Item]:
29 29 | return item
30 30 |
FAST001.py:32:22: FAST001 [*] FastAPI route with redundant `response_model` argument
|
32 | @app.post("/items/", response_model=Dict[str, Item])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST001
33 | async def create_item(item: Item) -> Dict[str, Item]:
34 | return item
|
= help: Remove argument
Unsafe fix
29 29 | return item
30 30 |
31 31 |
32 |-@app.post("/items/", response_model=Dict[str, Item])
32 |+@app.post("/items/")
33 33 | async def create_item(item: Item) -> Dict[str, Item]:
34 34 | return item
35 35 |
FAST001.py:37:22: FAST001 [*] FastAPI route with redundant `response_model` argument
|
37 | @app.post("/items/", response_model=str)
| ^^^^^^^^^^^^^^^^^^ FAST001
38 | async def create_item(item: Item) -> str:
39 | return item
|
= help: Remove argument
Unsafe fix
34 34 | return item
35 35 |
36 36 |
37 |-@app.post("/items/", response_model=str)
37 |+@app.post("/items/")
38 38 | async def create_item(item: Item) -> str:
39 39 | return item
40 40 |
FAST001.py:42:21: FAST001 [*] FastAPI route with redundant `response_model` argument
|
42 | @app.get("/items/", response_model=Item)
| ^^^^^^^^^^^^^^^^^^^ FAST001
43 | async def create_item(item: Item) -> Item:
44 | return item
|
= help: Remove argument
Unsafe fix
39 39 | return item
40 40 |
41 41 |
42 |-@app.get("/items/", response_model=Item)
42 |+@app.get("/items/")
43 43 | async def create_item(item: Item) -> Item:
44 44 | return item
45 45 |
FAST001.py:47:21: FAST001 [*] FastAPI route with redundant `response_model` argument
|
47 | @app.get("/items/", response_model=Item)
| ^^^^^^^^^^^^^^^^^^^ FAST001
48 | @app.post("/items/", response_model=Item)
49 | async def create_item(item: Item) -> Item:
|
= help: Remove argument
Unsafe fix
44 44 | return item
45 45 |
46 46 |
47 |-@app.get("/items/", response_model=Item)
47 |+@app.get("/items/")
48 48 | @app.post("/items/", response_model=Item)
49 49 | async def create_item(item: Item) -> Item:
50 50 | return item
FAST001.py:48:22: FAST001 [*] FastAPI route with redundant `response_model` argument
|
47 | @app.get("/items/", response_model=Item)
48 | @app.post("/items/", response_model=Item)
| ^^^^^^^^^^^^^^^^^^^ FAST001
49 | async def create_item(item: Item) -> Item:
50 | return item
|
= help: Remove argument
Unsafe fix
45 45 |
46 46 |
47 47 | @app.get("/items/", response_model=Item)
48 |-@app.post("/items/", response_model=Item)
48 |+@app.post("/items/")
49 49 | async def create_item(item: Item) -> Item:
50 50 | return item
51 51 |
FAST001.py:53:24: FAST001 [*] FastAPI route with redundant `response_model` argument
|
53 | @router.get("/items/", response_model=Item)
| ^^^^^^^^^^^^^^^^^^^ FAST001
54 | async def create_item(item: Item) -> Item:
55 | return item
|
= help: Remove argument
Unsafe fix
50 50 | return item
51 51 |
52 52 |
53 |-@router.get("/items/", response_model=Item)
53 |+@router.get("/items/")
54 54 | async def create_item(item: Item) -> Item:
55 55 | return item
56 56 |

View file

@ -1,6 +1,7 @@
#![allow(clippy::useless_format)]
pub mod airflow;
pub mod eradicate;
pub mod fastapi;
pub mod flake8_2020;
pub mod flake8_annotations;
pub mod flake8_async;

View file

@ -1238,6 +1238,7 @@ impl<'a> SemanticModel<'a> {
"dataclasses" => self.seen.insert(Modules::DATACLASSES),
"datetime" => self.seen.insert(Modules::DATETIME),
"django" => self.seen.insert(Modules::DJANGO),
"fastapi" => self.seen.insert(Modules::FASTAPI),
"logging" => self.seen.insert(Modules::LOGGING),
"mock" => self.seen.insert(Modules::MOCK),
"numpy" => self.seen.insert(Modules::NUMPY),
@ -1824,6 +1825,7 @@ bitflags! {
const BUILTINS = 1 << 18;
const CONTEXTVARS = 1 << 19;
const ANYIO = 1 << 20;
const FASTAPI = 1 << 21;
}
}

5
ruff.schema.json generated
View file

@ -3066,6 +3066,11 @@
"FA10",
"FA100",
"FA102",
"FAST",
"FAST0",
"FAST00",
"FAST001",
"FAST002",
"FBT",
"FBT0",
"FBT00",