mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-16 16:40:19 +00:00
[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:
parent
82355712c3
commit
053243635c
14 changed files with 1009 additions and 5 deletions
110
crates/ruff_linter/resources/test/fixtures/fastapi/FAST001.py
vendored
Normal file
110
crates/ruff_linter/resources/test/fixtures/fastapi/FAST001.py
vendored
Normal 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
|
68
crates/ruff_linter/resources/test/fixtures/fastapi/FAST002.py
vendored
Normal file
68
crates/ruff_linter/resources/test/fixtures/fastapi/FAST002.py
vendored
Normal 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
|
|
@ -8,11 +8,11 @@ use ruff_text_size::Ranged;
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
use crate::registry::Rule;
|
use crate::registry::Rule;
|
||||||
use crate::rules::{
|
use crate::rules::{
|
||||||
airflow, flake8_async, flake8_bandit, flake8_boolean_trap, flake8_bugbear, flake8_builtins,
|
airflow, fastapi, flake8_async, flake8_bandit, flake8_boolean_trap, flake8_bugbear,
|
||||||
flake8_debugger, flake8_django, flake8_errmsg, flake8_import_conventions, flake8_pie,
|
flake8_builtins, flake8_debugger, flake8_django, flake8_errmsg, flake8_import_conventions,
|
||||||
flake8_pyi, flake8_pytest_style, flake8_raise, flake8_return, flake8_simplify, flake8_slots,
|
flake8_pie, flake8_pyi, flake8_pytest_style, flake8_raise, flake8_return, flake8_simplify,
|
||||||
flake8_tidy_imports, flake8_type_checking, mccabe, pandas_vet, pep8_naming, perflint,
|
flake8_slots, flake8_tidy_imports, flake8_type_checking, mccabe, pandas_vet, pep8_naming,
|
||||||
pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade, refurb, ruff, tryceratops,
|
perflint, pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade, refurb, ruff, tryceratops,
|
||||||
};
|
};
|
||||||
use crate::settings::types::PythonVersion;
|
use crate::settings::types::PythonVersion;
|
||||||
|
|
||||||
|
@ -88,6 +88,12 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||||
if checker.enabled(Rule::DjangoNonLeadingReceiverDecorator) {
|
if checker.enabled(Rule::DjangoNonLeadingReceiverDecorator) {
|
||||||
flake8_django::rules::non_leading_receiver_decorator(checker, decorator_list);
|
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 checker.enabled(Rule::AmbiguousFunctionName) {
|
||||||
if let Some(diagnostic) = pycodestyle::rules::ambiguous_function_name(name) {
|
if let Some(diagnostic) = pycodestyle::rules::ambiguous_function_name(name) {
|
||||||
checker.diagnostics.push(diagnostic);
|
checker.diagnostics.push(diagnostic);
|
||||||
|
|
|
@ -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, "003") => (RuleGroup::Stable, rules::numpy::rules::NumpyDeprecatedFunction),
|
||||||
(Numpy, "201") => (RuleGroup::Stable, rules::numpy::rules::Numpy2Deprecation),
|
(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
|
||||||
(Pydoclint, "501") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingException),
|
(Pydoclint, "501") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingException),
|
||||||
(Pydoclint, "502") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringExtraneousException),
|
(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, "030") => (RuleGroup::Preview, rules::ruff::rules::AssertWithPrintMessage),
|
||||||
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
|
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
|
||||||
(Ruff, "101") => (RuleGroup::Preview, rules::ruff::rules::RedirectedNOQA),
|
(Ruff, "101") => (RuleGroup::Preview, rules::ruff::rules::RedirectedNOQA),
|
||||||
|
|
||||||
(Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml),
|
(Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml),
|
||||||
#[cfg(any(feature = "test-rules", test))]
|
#[cfg(any(feature = "test-rules", test))]
|
||||||
(Ruff, "900") => (RuleGroup::Stable, rules::ruff::rules::StableTestRule),
|
(Ruff, "900") => (RuleGroup::Stable, rules::ruff::rules::StableTestRule),
|
||||||
|
|
|
@ -193,6 +193,9 @@ pub enum Linter {
|
||||||
/// NumPy-specific rules
|
/// NumPy-specific rules
|
||||||
#[prefix = "NPY"]
|
#[prefix = "NPY"]
|
||||||
Numpy,
|
Numpy,
|
||||||
|
/// [FastAPI](https://pypi.org/project/fastapi/)
|
||||||
|
#[prefix = "FAST"]
|
||||||
|
FastApi,
|
||||||
/// [Airflow](https://pypi.org/project/apache-airflow/)
|
/// [Airflow](https://pypi.org/project/apache-airflow/)
|
||||||
#[prefix = "AIR"]
|
#[prefix = "AIR"]
|
||||||
Airflow,
|
Airflow,
|
||||||
|
|
27
crates/ruff_linter/src/rules/fastapi/mod.rs
Normal file
27
crates/ruff_linter/src/rules/fastapi/mod.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)) =
|
||||||
|
(¶meter.parameter.annotation, ¶meter.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
44
crates/ruff_linter/src/rules/fastapi/rules/mod.rs
Normal file
44
crates/ruff_linter/src/rules/fastapi/rules/mod.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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 |
|
|
@ -1,6 +1,7 @@
|
||||||
#![allow(clippy::useless_format)]
|
#![allow(clippy::useless_format)]
|
||||||
pub mod airflow;
|
pub mod airflow;
|
||||||
pub mod eradicate;
|
pub mod eradicate;
|
||||||
|
pub mod fastapi;
|
||||||
pub mod flake8_2020;
|
pub mod flake8_2020;
|
||||||
pub mod flake8_annotations;
|
pub mod flake8_annotations;
|
||||||
pub mod flake8_async;
|
pub mod flake8_async;
|
||||||
|
|
|
@ -1238,6 +1238,7 @@ impl<'a> SemanticModel<'a> {
|
||||||
"dataclasses" => self.seen.insert(Modules::DATACLASSES),
|
"dataclasses" => self.seen.insert(Modules::DATACLASSES),
|
||||||
"datetime" => self.seen.insert(Modules::DATETIME),
|
"datetime" => self.seen.insert(Modules::DATETIME),
|
||||||
"django" => self.seen.insert(Modules::DJANGO),
|
"django" => self.seen.insert(Modules::DJANGO),
|
||||||
|
"fastapi" => self.seen.insert(Modules::FASTAPI),
|
||||||
"logging" => self.seen.insert(Modules::LOGGING),
|
"logging" => self.seen.insert(Modules::LOGGING),
|
||||||
"mock" => self.seen.insert(Modules::MOCK),
|
"mock" => self.seen.insert(Modules::MOCK),
|
||||||
"numpy" => self.seen.insert(Modules::NUMPY),
|
"numpy" => self.seen.insert(Modules::NUMPY),
|
||||||
|
@ -1824,6 +1825,7 @@ bitflags! {
|
||||||
const BUILTINS = 1 << 18;
|
const BUILTINS = 1 << 18;
|
||||||
const CONTEXTVARS = 1 << 19;
|
const CONTEXTVARS = 1 << 19;
|
||||||
const ANYIO = 1 << 20;
|
const ANYIO = 1 << 20;
|
||||||
|
const FASTAPI = 1 << 21;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
5
ruff.schema.json
generated
5
ruff.schema.json
generated
|
@ -3066,6 +3066,11 @@
|
||||||
"FA10",
|
"FA10",
|
||||||
"FA100",
|
"FA100",
|
||||||
"FA102",
|
"FA102",
|
||||||
|
"FAST",
|
||||||
|
"FAST0",
|
||||||
|
"FAST00",
|
||||||
|
"FAST001",
|
||||||
|
"FAST002",
|
||||||
"FBT",
|
"FBT",
|
||||||
"FBT0",
|
"FBT0",
|
||||||
"FBT00",
|
"FBT00",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue