mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
[flake8-async
] Implement blocking-http-call-httpx
(ASYNC212
) (#20091)
## Summary Adds new rule to find and report use of `httpx.Client` in synchronous functions. See issue #8451 ## Test Plan New snapshots for `ASYNC212.py` with `cargo insta test`.
This commit is contained in:
parent
d75ef3823c
commit
af259faed5
8 changed files with 396 additions and 0 deletions
75
crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC212.py
vendored
Normal file
75
crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC212.py
vendored
Normal file
|
@ -0,0 +1,75 @@
|
|||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
def foo():
|
||||
client = httpx.Client()
|
||||
client.close() # Ok
|
||||
client.delete() # Ok
|
||||
client.get() # Ok
|
||||
client.head() # Ok
|
||||
client.options() # Ok
|
||||
client.patch() # Ok
|
||||
client.post() # Ok
|
||||
client.put() # Ok
|
||||
client.request() # Ok
|
||||
client.send() # Ok
|
||||
client.stream() # Ok
|
||||
|
||||
client.anything() # Ok
|
||||
client.build_request() # Ok
|
||||
client.is_closed # Ok
|
||||
|
||||
|
||||
async def foo():
|
||||
client = httpx.Client()
|
||||
client.close() # ASYNC212
|
||||
client.delete() # ASYNC212
|
||||
client.get() # ASYNC212
|
||||
client.head() # ASYNC212
|
||||
client.options() # ASYNC212
|
||||
client.patch() # ASYNC212
|
||||
client.post() # ASYNC212
|
||||
client.put() # ASYNC212
|
||||
client.request() # ASYNC212
|
||||
client.send() # ASYNC212
|
||||
client.stream() # ASYNC212
|
||||
|
||||
client.anything() # Ok
|
||||
client.build_request() # Ok
|
||||
client.is_closed # Ok
|
||||
|
||||
|
||||
async def foo(client: httpx.Client):
|
||||
client.request() # ASYNC212
|
||||
client.anything() # Ok
|
||||
|
||||
|
||||
async def foo(client: httpx.Client | None):
|
||||
client.request() # ASYNC212
|
||||
client.anything() # Ok
|
||||
|
||||
|
||||
async def foo(client: Optional[httpx.Client]):
|
||||
client.request() # ASYNC212
|
||||
client.anything() # Ok
|
||||
|
||||
|
||||
async def foo():
|
||||
client: httpx.Client = ...
|
||||
client.request() # ASYNC212
|
||||
client.anything() # Ok
|
||||
|
||||
|
||||
global_client = httpx.Client()
|
||||
|
||||
|
||||
async def foo():
|
||||
global_client.request() # ASYNC212
|
||||
global_client.anything() # Ok
|
||||
|
||||
|
||||
async def foo():
|
||||
async with httpx.AsyncClient() as client:
|
||||
await client.get() # Ok
|
|
@ -660,6 +660,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
|||
if checker.is_rule_enabled(Rule::BlockingHttpCallInAsyncFunction) {
|
||||
flake8_async::rules::blocking_http_call(checker, call);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::BlockingHttpCallHttpxInAsyncFunction) {
|
||||
flake8_async::rules::blocking_http_call_httpx(checker, call);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::BlockingOpenCallInAsyncFunction) {
|
||||
flake8_async::rules::blocking_open_call(checker, call);
|
||||
}
|
||||
|
|
|
@ -336,6 +336,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
|||
(Flake8Async, "115") => (RuleGroup::Stable, rules::flake8_async::rules::AsyncZeroSleep),
|
||||
(Flake8Async, "116") => (RuleGroup::Preview, rules::flake8_async::rules::LongSleepNotForever),
|
||||
(Flake8Async, "210") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingHttpCallInAsyncFunction),
|
||||
(Flake8Async, "212") => (RuleGroup::Preview, rules::flake8_async::rules::BlockingHttpCallHttpxInAsyncFunction),
|
||||
(Flake8Async, "220") => (RuleGroup::Stable, rules::flake8_async::rules::CreateSubprocessInAsyncFunction),
|
||||
(Flake8Async, "221") => (RuleGroup::Stable, rules::flake8_async::rules::RunProcessInAsyncFunction),
|
||||
(Flake8Async, "222") => (RuleGroup::Stable, rules::flake8_async::rules::WaitForProcessInAsyncFunction),
|
||||
|
|
|
@ -23,6 +23,7 @@ mod tests {
|
|||
#[test_case(Rule::AsyncZeroSleep, Path::new("ASYNC115.py"))]
|
||||
#[test_case(Rule::LongSleepNotForever, Path::new("ASYNC116.py"))]
|
||||
#[test_case(Rule::BlockingHttpCallInAsyncFunction, Path::new("ASYNC210.py"))]
|
||||
#[test_case(Rule::BlockingHttpCallHttpxInAsyncFunction, Path::new("ASYNC212.py"))]
|
||||
#[test_case(Rule::CreateSubprocessInAsyncFunction, Path::new("ASYNC22x.py"))]
|
||||
#[test_case(Rule::RunProcessInAsyncFunction, Path::new("ASYNC22x.py"))]
|
||||
#[test_case(Rule::WaitForProcessInAsyncFunction, Path::new("ASYNC22x.py"))]
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
use ruff_python_ast::{self as ast, Expr, ExprCall};
|
||||
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_semantic::analyze::typing::{TypeChecker, check_type, traverse_union_and_optional};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::Violation;
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks that async functions do not use blocking httpx clients.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Blocking an async function via a blocking HTTP call will block the entire
|
||||
/// event loop, preventing it from executing other tasks while waiting for the
|
||||
/// HTTP response, negating the benefits of asynchronous programming.
|
||||
///
|
||||
/// Instead of using the blocking `httpx` client, use the asynchronous client.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import httpx
|
||||
///
|
||||
///
|
||||
/// async def fetch():
|
||||
/// client = httpx.Client()
|
||||
/// response = client.get(...)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import httpx
|
||||
///
|
||||
///
|
||||
/// async def fetch():
|
||||
/// async with httpx.AsyncClient() as client:
|
||||
/// response = await client.get(...)
|
||||
/// ```
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct BlockingHttpCallHttpxInAsyncFunction {
|
||||
name: String,
|
||||
call: String,
|
||||
}
|
||||
|
||||
impl Violation for BlockingHttpCallHttpxInAsyncFunction {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"Blocking httpx method {name}.{call}() in async context, use httpx.AsyncClient",
|
||||
name = self.name,
|
||||
call = self.call,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct HttpxClientChecker;
|
||||
|
||||
impl TypeChecker for HttpxClientChecker {
|
||||
fn match_annotation(
|
||||
annotation: &ruff_python_ast::Expr,
|
||||
semantic: &ruff_python_semantic::SemanticModel,
|
||||
) -> bool {
|
||||
// match base annotation directly
|
||||
if semantic
|
||||
.resolve_qualified_name(annotation)
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["httpx", "Client"]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// otherwise traverse any union or optional annotation
|
||||
let mut found = false;
|
||||
traverse_union_and_optional(
|
||||
&mut |inner_expr, _| {
|
||||
if semantic
|
||||
.resolve_qualified_name(inner_expr)
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(qualified_name.segments(), ["httpx", "Client"])
|
||||
})
|
||||
{
|
||||
found = true;
|
||||
}
|
||||
},
|
||||
semantic,
|
||||
annotation,
|
||||
);
|
||||
found
|
||||
}
|
||||
|
||||
fn match_initializer(
|
||||
initializer: &ruff_python_ast::Expr,
|
||||
semantic: &ruff_python_semantic::SemanticModel,
|
||||
) -> bool {
|
||||
let Expr::Call(ExprCall { func, .. }) = initializer else {
|
||||
return false;
|
||||
};
|
||||
|
||||
semantic
|
||||
.resolve_qualified_name(func)
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["httpx", "Client"]))
|
||||
}
|
||||
}
|
||||
|
||||
/// ASYNC212
|
||||
pub(crate) fn blocking_http_call_httpx(checker: &Checker, call: &ExprCall) {
|
||||
let semantic = checker.semantic();
|
||||
if !semantic.in_async_context() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(ast::ExprAttribute { value, attr, .. }) = call.func.as_attribute_expr() else {
|
||||
return;
|
||||
};
|
||||
let Some(name) = value.as_name_expr() else {
|
||||
return;
|
||||
};
|
||||
let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if check_type::<HttpxClientChecker>(binding, semantic) {
|
||||
if matches!(
|
||||
attr.id.as_str(),
|
||||
"close"
|
||||
| "delete"
|
||||
| "get"
|
||||
| "head"
|
||||
| "options"
|
||||
| "patch"
|
||||
| "post"
|
||||
| "put"
|
||||
| "request"
|
||||
| "send"
|
||||
| "stream"
|
||||
) {
|
||||
checker.report_diagnostic(
|
||||
BlockingHttpCallHttpxInAsyncFunction {
|
||||
name: name.id.to_string(),
|
||||
call: attr.id.to_string(),
|
||||
},
|
||||
call.func.range(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ pub(crate) use async_busy_wait::*;
|
|||
pub(crate) use async_function_with_timeout::*;
|
||||
pub(crate) use async_zero_sleep::*;
|
||||
pub(crate) use blocking_http_call::*;
|
||||
pub(crate) use blocking_http_call_httpx::*;
|
||||
pub(crate) use blocking_open_call::*;
|
||||
pub(crate) use blocking_process_invocation::*;
|
||||
pub(crate) use blocking_sleep::*;
|
||||
|
@ -13,6 +14,7 @@ mod async_busy_wait;
|
|||
mod async_function_with_timeout;
|
||||
mod async_zero_sleep;
|
||||
mod blocking_http_call;
|
||||
mod blocking_http_call_httpx;
|
||||
mod blocking_open_call;
|
||||
mod blocking_process_invocation;
|
||||
mod blocking_sleep;
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
|
||||
---
|
||||
ASYNC212 Blocking httpx method client.close() in async context, use httpx.AsyncClient
|
||||
--> ASYNC212.py:27:5
|
||||
|
|
||||
25 | async def foo():
|
||||
26 | client = httpx.Client()
|
||||
27 | client.close() # ASYNC212
|
||||
| ^^^^^^^^^^^^
|
||||
28 | client.delete() # ASYNC212
|
||||
29 | client.get() # ASYNC212
|
||||
|
|
||||
|
||||
ASYNC212 Blocking httpx method client.delete() in async context, use httpx.AsyncClient
|
||||
--> ASYNC212.py:28:5
|
||||
|
|
||||
26 | client = httpx.Client()
|
||||
27 | client.close() # ASYNC212
|
||||
28 | client.delete() # ASYNC212
|
||||
| ^^^^^^^^^^^^^
|
||||
29 | client.get() # ASYNC212
|
||||
30 | client.head() # ASYNC212
|
||||
|
|
||||
|
||||
ASYNC212 Blocking httpx method client.get() in async context, use httpx.AsyncClient
|
||||
--> ASYNC212.py:29:5
|
||||
|
|
||||
27 | client.close() # ASYNC212
|
||||
28 | client.delete() # ASYNC212
|
||||
29 | client.get() # ASYNC212
|
||||
| ^^^^^^^^^^
|
||||
30 | client.head() # ASYNC212
|
||||
31 | client.options() # ASYNC212
|
||||
|
|
||||
|
||||
ASYNC212 Blocking httpx method client.head() in async context, use httpx.AsyncClient
|
||||
--> ASYNC212.py:30:5
|
||||
|
|
||||
28 | client.delete() # ASYNC212
|
||||
29 | client.get() # ASYNC212
|
||||
30 | client.head() # ASYNC212
|
||||
| ^^^^^^^^^^^
|
||||
31 | client.options() # ASYNC212
|
||||
32 | client.patch() # ASYNC212
|
||||
|
|
||||
|
||||
ASYNC212 Blocking httpx method client.options() in async context, use httpx.AsyncClient
|
||||
--> ASYNC212.py:31:5
|
||||
|
|
||||
29 | client.get() # ASYNC212
|
||||
30 | client.head() # ASYNC212
|
||||
31 | client.options() # ASYNC212
|
||||
| ^^^^^^^^^^^^^^
|
||||
32 | client.patch() # ASYNC212
|
||||
33 | client.post() # ASYNC212
|
||||
|
|
||||
|
||||
ASYNC212 Blocking httpx method client.patch() in async context, use httpx.AsyncClient
|
||||
--> ASYNC212.py:32:5
|
||||
|
|
||||
30 | client.head() # ASYNC212
|
||||
31 | client.options() # ASYNC212
|
||||
32 | client.patch() # ASYNC212
|
||||
| ^^^^^^^^^^^^
|
||||
33 | client.post() # ASYNC212
|
||||
34 | client.put() # ASYNC212
|
||||
|
|
||||
|
||||
ASYNC212 Blocking httpx method client.post() in async context, use httpx.AsyncClient
|
||||
--> ASYNC212.py:33:5
|
||||
|
|
||||
31 | client.options() # ASYNC212
|
||||
32 | client.patch() # ASYNC212
|
||||
33 | client.post() # ASYNC212
|
||||
| ^^^^^^^^^^^
|
||||
34 | client.put() # ASYNC212
|
||||
35 | client.request() # ASYNC212
|
||||
|
|
||||
|
||||
ASYNC212 Blocking httpx method client.put() in async context, use httpx.AsyncClient
|
||||
--> ASYNC212.py:34:5
|
||||
|
|
||||
32 | client.patch() # ASYNC212
|
||||
33 | client.post() # ASYNC212
|
||||
34 | client.put() # ASYNC212
|
||||
| ^^^^^^^^^^
|
||||
35 | client.request() # ASYNC212
|
||||
36 | client.send() # ASYNC212
|
||||
|
|
||||
|
||||
ASYNC212 Blocking httpx method client.request() in async context, use httpx.AsyncClient
|
||||
--> ASYNC212.py:35:5
|
||||
|
|
||||
33 | client.post() # ASYNC212
|
||||
34 | client.put() # ASYNC212
|
||||
35 | client.request() # ASYNC212
|
||||
| ^^^^^^^^^^^^^^
|
||||
36 | client.send() # ASYNC212
|
||||
37 | client.stream() # ASYNC212
|
||||
|
|
||||
|
||||
ASYNC212 Blocking httpx method client.send() in async context, use httpx.AsyncClient
|
||||
--> ASYNC212.py:36:5
|
||||
|
|
||||
34 | client.put() # ASYNC212
|
||||
35 | client.request() # ASYNC212
|
||||
36 | client.send() # ASYNC212
|
||||
| ^^^^^^^^^^^
|
||||
37 | client.stream() # ASYNC212
|
||||
|
|
||||
|
||||
ASYNC212 Blocking httpx method client.stream() in async context, use httpx.AsyncClient
|
||||
--> ASYNC212.py:37:5
|
||||
|
|
||||
35 | client.request() # ASYNC212
|
||||
36 | client.send() # ASYNC212
|
||||
37 | client.stream() # ASYNC212
|
||||
| ^^^^^^^^^^^^^
|
||||
38 |
|
||||
39 | client.anything() # Ok
|
||||
|
|
||||
|
||||
ASYNC212 Blocking httpx method client.request() in async context, use httpx.AsyncClient
|
||||
--> ASYNC212.py:45:5
|
||||
|
|
||||
44 | async def foo(client: httpx.Client):
|
||||
45 | client.request() # ASYNC212
|
||||
| ^^^^^^^^^^^^^^
|
||||
46 | client.anything() # Ok
|
||||
|
|
||||
|
||||
ASYNC212 Blocking httpx method client.request() in async context, use httpx.AsyncClient
|
||||
--> ASYNC212.py:50:5
|
||||
|
|
||||
49 | async def foo(client: httpx.Client | None):
|
||||
50 | client.request() # ASYNC212
|
||||
| ^^^^^^^^^^^^^^
|
||||
51 | client.anything() # Ok
|
||||
|
|
||||
|
||||
ASYNC212 Blocking httpx method client.request() in async context, use httpx.AsyncClient
|
||||
--> ASYNC212.py:55:5
|
||||
|
|
||||
54 | async def foo(client: Optional[httpx.Client]):
|
||||
55 | client.request() # ASYNC212
|
||||
| ^^^^^^^^^^^^^^
|
||||
56 | client.anything() # Ok
|
||||
|
|
||||
|
||||
ASYNC212 Blocking httpx method client.request() in async context, use httpx.AsyncClient
|
||||
--> ASYNC212.py:61:5
|
||||
|
|
||||
59 | async def foo():
|
||||
60 | client: httpx.Client = ...
|
||||
61 | client.request() # ASYNC212
|
||||
| ^^^^^^^^^^^^^^
|
||||
62 | client.anything() # Ok
|
||||
|
|
||||
|
||||
ASYNC212 Blocking httpx method global_client.request() in async context, use httpx.AsyncClient
|
||||
--> ASYNC212.py:69:5
|
||||
|
|
||||
68 | async def foo():
|
||||
69 | global_client.request() # ASYNC212
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
70 | global_client.anything() # Ok
|
||||
|
|
1
ruff.schema.json
generated
1
ruff.schema.json
generated
|
@ -3013,6 +3013,7 @@
|
|||
"ASYNC2",
|
||||
"ASYNC21",
|
||||
"ASYNC210",
|
||||
"ASYNC212",
|
||||
"ASYNC22",
|
||||
"ASYNC220",
|
||||
"ASYNC221",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue