[ruff-0.8] [FAST] Further improve docs for fast-api-non-annotated-depencency (FAST002) (#14467)

This commit is contained in:
Alex Waygood 2024-11-20 10:48:26 +00:00 committed by Micha Reiser
parent b2bb119c6a
commit 4ccacc80f9
4 changed files with 380 additions and 16 deletions

View file

@ -25,4 +25,20 @@ mod tests {
assert_messages!(snapshot, diagnostics);
Ok(())
}
// FAST002 autofixes use `typing_extensions` on Python 3.8,
// since `typing.Annotated` was added in Python 3.9
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002.py"))]
fn rules_py38(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}_py38", rule_code.as_ref(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("fastapi").join(path).as_path(),
&settings::LinterSettings {
target_version: settings::types::PythonVersion::Py38,
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
}

View file

@ -20,6 +20,10 @@ use crate::settings::types::PythonVersion;
/// everywhere helps ensure consistency and clarity in defining dependencies
/// and parameters.
///
/// `Annotated` was added to the `typing` module in Python 3.9; however,
/// the third-party [`typing_extensions`] package provides a backport that can be
/// used on older versions of Python.
///
/// ## Example
///
/// ```python
@ -58,8 +62,11 @@ use crate::settings::types::PythonVersion;
///
/// [fastAPI documentation]: https://fastapi.tiangolo.com/tutorial/query-params-str-validations/?h=annotated#advantages-of-annotated
/// [typing.Annotated]: https://docs.python.org/3/library/typing.html#typing.Annotated
/// [typing_extensions]: https://typing-extensions.readthedocs.io/en/stable/
#[violation]
pub struct FastApiNonAnnotatedDependency;
pub struct FastApiNonAnnotatedDependency {
py_version: PythonVersion,
}
impl Violation for FastApiNonAnnotatedDependency {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
@ -70,7 +77,12 @@ impl Violation for FastApiNonAnnotatedDependency {
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Annotated`".to_string())
let title = if self.py_version >= PythonVersion::Py39 {
"Replace with `typing.Annotated`"
} else {
"Replace with `typing_extensions.Annotated`"
};
Some(title.to_string())
}
}
@ -137,7 +149,12 @@ fn create_diagnostic(
parameter: &ast::ParameterWithDefault,
safe_to_update: bool,
) {
let mut diagnostic = Diagnostic::new(FastApiNonAnnotatedDependency, parameter.range);
let mut diagnostic = Diagnostic::new(
FastApiNonAnnotatedDependency {
py_version: checker.settings.target_version,
},
parameter.range,
);
if safe_to_update {
if let (Some(annotation), Some(default)) =

View file

@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/fastapi/mod.rs
snapshot_kind: text
---
FAST002.py:24:5: FAST002 [*] FastAPI dependency without `Annotated`
|
@ -11,7 +10,7 @@ FAST002.py:24:5: FAST002 [*] FastAPI dependency without `Annotated`
25 | some_security_param: str = Security(get_oauth2_user),
26 | ):
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
@ -40,7 +39,7 @@ FAST002.py:25:5: FAST002 [*] FastAPI dependency without `Annotated`
26 | ):
27 | pass
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
@ -69,7 +68,7 @@ FAST002.py:32:5: FAST002 [*] FastAPI dependency without `Annotated`
33 | some_path_param: str = Path(),
34 | some_body_param: str = Body("foo"),
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
@ -98,7 +97,7 @@ FAST002.py:33:5: FAST002 [*] FastAPI dependency without `Annotated`
34 | some_body_param: str = Body("foo"),
35 | some_cookie_param: str = Cookie(),
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
@ -127,7 +126,7 @@ FAST002.py:34:5: FAST002 [*] FastAPI dependency without `Annotated`
35 | some_cookie_param: str = Cookie(),
36 | some_header_param: int = Header(default=5),
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
@ -156,7 +155,7 @@ FAST002.py:35:5: FAST002 [*] FastAPI dependency without `Annotated`
36 | some_header_param: int = Header(default=5),
37 | some_file_param: UploadFile = File(),
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
@ -185,7 +184,7 @@ FAST002.py:36:5: FAST002 [*] FastAPI dependency without `Annotated`
37 | some_file_param: UploadFile = File(),
38 | some_form_param: str = Form(),
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
@ -214,7 +213,7 @@ FAST002.py:37:5: FAST002 [*] FastAPI dependency without `Annotated`
38 | some_form_param: str = Form(),
39 | ):
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
@ -243,7 +242,7 @@ FAST002.py:38:5: FAST002 [*] FastAPI dependency without `Annotated`
39 | ):
40 | # do stuff
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
@ -272,7 +271,7 @@ FAST002.py:47:5: FAST002 [*] FastAPI dependency without `Annotated`
48 | ):
49 | pass
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
@ -301,7 +300,7 @@ FAST002.py:53:5: FAST002 [*] FastAPI dependency without `Annotated`
54 | skip: int = 0,
55 | limit: int = 10,
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
@ -330,4 +329,4 @@ FAST002.py:67:5: FAST002 FastAPI dependency without `Annotated`
68 | ):
69 | pass
|
= help: Replace with `Annotated`
= help: Replace with `typing.Annotated`

View file

@ -0,0 +1,332 @@
---
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 `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions 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 `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions 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 `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions 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 `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions 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 `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions 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 `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions 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 `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions 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 `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions 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 `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions 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
FAST002.py:47:5: FAST002 [*] FastAPI dependency without `Annotated`
|
45 | skip: int,
46 | limit: int,
47 | current_user: User = Depends(get_current_user),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
48 | ):
49 | pass
|
= help: Replace with `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
44 45 | def get_users(
45 46 | skip: int,
46 47 | limit: int,
47 |- current_user: User = Depends(get_current_user),
48 |+ current_user: Annotated[User, Depends(get_current_user)],
48 49 | ):
49 50 | pass
50 51 |
FAST002.py:53:5: FAST002 [*] FastAPI dependency without `Annotated`
|
51 | @app.get("/users/")
52 | def get_users(
53 | current_user: User = Depends(get_current_user),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
54 | skip: int = 0,
55 | limit: int = 10,
|
= help: Replace with `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
50 51 |
51 52 | @app.get("/users/")
52 53 | def get_users(
53 |- current_user: User = Depends(get_current_user),
54 |+ current_user: Annotated[User, Depends(get_current_user)],
54 55 | skip: int = 0,
55 56 | limit: int = 10,
56 57 | ):
FAST002.py:67:5: FAST002 FastAPI dependency without `Annotated`
|
65 | skip: int = 0,
66 | limit: int = 10,
67 | current_user: User = Depends(get_current_user),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
68 | ):
69 | pass
|
= help: Replace with `typing_extensions.Annotated`