[flake8-async] Update ASYNC109 to match upstream (#12236)

## Summary

Update the name of `ASYNC109` to match
[upstream](https://flake8-async.readthedocs.io/en/latest/rules.html).

Also update to the functionality to match upstream by supporting
additional context managers from `asyncio` and `anyio`. This doesn't
change any of the detection functionality, but recommends additional
context managers from `asyncio` and `anyio` depending on context.

Part of https://github.com/astral-sh/ruff/issues/12039.

## Test Plan

Added fixture for asyncio recommendation
This commit is contained in:
Auguste Lalande 2024-07-09 00:14:27 -04:00 committed by GitHub
parent 10f07d88a2
commit 16a63c88cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 147 additions and 27 deletions

View file

@ -0,0 +1,10 @@
async def func():
...
async def func(timeout):
...
async def func(timeout=10):
...

View file

@ -361,7 +361,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_builtins::rules::builtin_variable_shadowing(checker, name, name.range()); flake8_builtins::rules::builtin_variable_shadowing(checker, name, name.range());
} }
} }
if checker.enabled(Rule::TrioAsyncFunctionWithTimeout) { if checker.enabled(Rule::AsyncFunctionWithTimeout) {
flake8_async::rules::async_function_with_timeout(checker, function_def); flake8_async::rules::async_function_with_timeout(checker, function_def);
} }
if checker.enabled(Rule::ReimplementedOperator) { if checker.enabled(Rule::ReimplementedOperator) {

View file

@ -295,7 +295,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
// flake8-async // flake8-async
(Flake8Async, "100") => (RuleGroup::Stable, rules::flake8_async::rules::TrioTimeoutWithoutAwait), (Flake8Async, "100") => (RuleGroup::Stable, rules::flake8_async::rules::TrioTimeoutWithoutAwait),
(Flake8Async, "105") => (RuleGroup::Stable, rules::flake8_async::rules::TrioSyncCall), (Flake8Async, "105") => (RuleGroup::Stable, rules::flake8_async::rules::TrioSyncCall),
(Flake8Async, "109") => (RuleGroup::Stable, rules::flake8_async::rules::TrioAsyncFunctionWithTimeout), (Flake8Async, "109") => (RuleGroup::Stable, rules::flake8_async::rules::AsyncFunctionWithTimeout),
(Flake8Async, "110") => (RuleGroup::Stable, rules::flake8_async::rules::TrioUnneededSleep), (Flake8Async, "110") => (RuleGroup::Stable, rules::flake8_async::rules::TrioUnneededSleep),
(Flake8Async, "115") => (RuleGroup::Stable, rules::flake8_async::rules::TrioZeroSleepCall), (Flake8Async, "115") => (RuleGroup::Stable, rules::flake8_async::rules::TrioZeroSleepCall),
(Flake8Async, "116") => (RuleGroup::Preview, rules::flake8_async::rules::SleepForeverCall), (Flake8Async, "116") => (RuleGroup::Preview, rules::flake8_async::rules::SleepForeverCall),

View file

@ -1,5 +1,15 @@
use ruff_python_ast::name::QualifiedName; use ruff_python_ast::name::QualifiedName;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(super) enum AsyncModule {
/// `anyio`
AnyIo,
/// `asyncio`
AsyncIo,
/// `trio`
Trio,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(super) enum MethodName { pub(super) enum MethodName {
AcloseForcefully, AcloseForcefully,

View file

@ -9,14 +9,16 @@ mod tests {
use anyhow::Result; use anyhow::Result;
use test_case::test_case; use test_case::test_case;
use crate::assert_messages;
use crate::registry::Rule; use crate::registry::Rule;
use crate::settings::types::PreviewMode;
use crate::settings::LinterSettings; use crate::settings::LinterSettings;
use crate::test::test_path; use crate::test::test_path;
use crate::{assert_messages, settings};
#[test_case(Rule::TrioTimeoutWithoutAwait, Path::new("ASYNC100.py"))] #[test_case(Rule::TrioTimeoutWithoutAwait, Path::new("ASYNC100.py"))]
#[test_case(Rule::TrioSyncCall, Path::new("ASYNC105.py"))] #[test_case(Rule::TrioSyncCall, Path::new("ASYNC105.py"))]
#[test_case(Rule::TrioAsyncFunctionWithTimeout, Path::new("ASYNC109.py"))] #[test_case(Rule::AsyncFunctionWithTimeout, Path::new("ASYNC109_0.py"))]
#[test_case(Rule::AsyncFunctionWithTimeout, Path::new("ASYNC109_1.py"))]
#[test_case(Rule::TrioUnneededSleep, Path::new("ASYNC110.py"))] #[test_case(Rule::TrioUnneededSleep, Path::new("ASYNC110.py"))]
#[test_case(Rule::TrioZeroSleepCall, Path::new("ASYNC115.py"))] #[test_case(Rule::TrioZeroSleepCall, Path::new("ASYNC115.py"))]
#[test_case(Rule::SleepForeverCall, Path::new("ASYNC116.py"))] #[test_case(Rule::SleepForeverCall, Path::new("ASYNC116.py"))]
@ -35,4 +37,23 @@ mod tests {
assert_messages!(snapshot, diagnostics); assert_messages!(snapshot, diagnostics);
Ok(()) Ok(())
} }
#[test_case(Rule::AsyncFunctionWithTimeout, Path::new("ASYNC109_0.py"))]
#[test_case(Rule::AsyncFunctionWithTimeout, Path::new("ASYNC109_1.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_async").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
} }

View file

@ -5,38 +5,60 @@ use ruff_python_semantic::Modules;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::rules::flake8_async::helpers::AsyncModule;
use crate::settings::types::PreviewMode;
/// ## What it does /// ## What it does
/// Checks for `async` functions with a `timeout` argument. /// Checks for `async` functions with a `timeout` argument.
/// ///
/// ## Why is this bad? /// ## Why is this bad?
/// Rather than implementing asynchronous timeout behavior manually, prefer /// Rather than implementing asynchronous timeout behavior manually, prefer
/// trio's built-in timeout functionality, available as `trio.fail_after`, /// built-in timeout functionality, such as `asyncio.timeout`, `trio.fail_after`,
/// `trio.move_on_after`, `trio.fail_at`, and `trio.move_on_at`. /// or `anyio.move_on_after`, among others.
///
/// ## Known problems
/// To avoid false positives, this rule is only enabled if `trio` is imported
/// in the module.
/// ///
/// ## Example /// ## Example
/// ```python /// ```python
/// async def func(): /// async def long_running_task(timeout):
/// ...
///
///
/// async def main():
/// await long_running_task(timeout=2) /// await long_running_task(timeout=2)
/// ``` /// ```
/// ///
/// Use instead: /// Use instead:
/// ```python /// ```python
/// async def func(): /// async def long_running_task():
/// with trio.fail_after(2): /// ...
///
///
/// async def main():
/// with asyncio.timeout(2):
/// await long_running_task() /// await long_running_task()
/// ``` /// ```
///
/// [`asyncio` timeouts]: https://docs.python.org/3/library/asyncio-task.html#timeouts
/// [`anyio` timeouts]: https://anyio.readthedocs.io/en/stable/cancellation.html
/// [`trio` timeouts]: https://trio.readthedocs.io/en/stable/reference-core.html#cancellation-and-timeouts
#[violation] #[violation]
pub struct TrioAsyncFunctionWithTimeout; pub struct AsyncFunctionWithTimeout {
module: AsyncModule,
}
impl Violation for TrioAsyncFunctionWithTimeout { impl Violation for AsyncFunctionWithTimeout {
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
format!("Prefer `trio.fail_after` and `trio.move_on_after` over manual `async` timeout behavior") format!("Async function definition with a `timeout` parameter")
}
fn fix_title(&self) -> Option<String> {
let Self { module } = self;
let recommendation = match module {
AsyncModule::AnyIo => "anyio.fail_after",
AsyncModule::Trio => "trio.fail_after",
AsyncModule::AsyncIo => "asyncio.timeout",
};
Some(format!("Use `{recommendation}` instead"))
} }
} }
@ -50,18 +72,31 @@ pub(crate) fn async_function_with_timeout(
return; return;
} }
// If `trio` isn't in scope, avoid raising the diagnostic.
if !checker.semantic().seen_module(Modules::TRIO) {
return;
}
// If the function doesn't have a `timeout` parameter, avoid raising the diagnostic. // If the function doesn't have a `timeout` parameter, avoid raising the diagnostic.
let Some(timeout) = function_def.parameters.find("timeout") else { let Some(timeout) = function_def.parameters.find("timeout") else {
return; return;
}; };
// Get preferred module.
let module = if checker.semantic().seen_module(Modules::ANYIO) {
AsyncModule::AnyIo
} else if checker.semantic().seen_module(Modules::TRIO) {
AsyncModule::Trio
} else {
AsyncModule::AsyncIo
};
if matches!(checker.settings.preview, PreviewMode::Disabled) {
if matches!(module, AsyncModule::Trio) {
checker.diagnostics.push(Diagnostic::new( checker.diagnostics.push(Diagnostic::new(
TrioAsyncFunctionWithTimeout, AsyncFunctionWithTimeout { module },
timeout.range(), timeout.range(),
)); ));
} }
} else {
checker.diagnostics.push(Diagnostic::new(
AsyncFunctionWithTimeout { module },
timeout.range(),
));
}
}

View file

@ -1,16 +1,18 @@
--- ---
source: crates/ruff_linter/src/rules/flake8_async/mod.rs source: crates/ruff_linter/src/rules/flake8_async/mod.rs
--- ---
ASYNC109.py:8:16: ASYNC109 Prefer `trio.fail_after` and `trio.move_on_after` over manual `async` timeout behavior ASYNC109_0.py:8:16: ASYNC109 Async function definition with a `timeout` parameter
| |
8 | async def func(timeout): 8 | async def func(timeout):
| ^^^^^^^ ASYNC109 | ^^^^^^^ ASYNC109
9 | ... 9 | ...
| |
= help: Use `trio.fail_after` instead
ASYNC109.py:12:16: ASYNC109 Prefer `trio.fail_after` and `trio.move_on_after` over manual `async` timeout behavior ASYNC109_0.py:12:16: ASYNC109 Async function definition with a `timeout` parameter
| |
12 | async def func(timeout=10): 12 | async def func(timeout=10):
| ^^^^^^^^^^ ASYNC109 | ^^^^^^^^^^ ASYNC109
13 | ... 13 | ...
| |
= help: Use `trio.fail_after` instead

View file

@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
---

View file

@ -0,0 +1,18 @@
---
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
---
ASYNC109_0.py:8:16: ASYNC109 Async function definition with a `timeout` parameter
|
8 | async def func(timeout):
| ^^^^^^^ ASYNC109
9 | ...
|
= help: Use `trio.fail_after` instead
ASYNC109_0.py:12:16: ASYNC109 Async function definition with a `timeout` parameter
|
12 | async def func(timeout=10):
| ^^^^^^^^^^ ASYNC109
13 | ...
|
= help: Use `trio.fail_after` instead

View file

@ -0,0 +1,18 @@
---
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
---
ASYNC109_1.py:5:16: ASYNC109 Async function definition with a `timeout` parameter
|
5 | async def func(timeout):
| ^^^^^^^ ASYNC109
6 | ...
|
= help: Use `asyncio.timeout` instead
ASYNC109_1.py:9:16: ASYNC109 Async function definition with a `timeout` parameter
|
9 | async def func(timeout=10):
| ^^^^^^^^^^ ASYNC109
10 | ...
|
= help: Use `asyncio.timeout` instead

View file

@ -1231,6 +1231,7 @@ impl<'a> SemanticModel<'a> {
pub fn add_module(&mut self, module: &str) { pub fn add_module(&mut self, module: &str) {
match module { match module {
"_typeshed" => self.seen.insert(Modules::TYPESHED), "_typeshed" => self.seen.insert(Modules::TYPESHED),
"anyio" => self.seen.insert(Modules::ANYIO),
"builtins" => self.seen.insert(Modules::BUILTINS), "builtins" => self.seen.insert(Modules::BUILTINS),
"collections" => self.seen.insert(Modules::COLLECTIONS), "collections" => self.seen.insert(Modules::COLLECTIONS),
"contextvars" => self.seen.insert(Modules::CONTEXTVARS), "contextvars" => self.seen.insert(Modules::CONTEXTVARS),
@ -1822,6 +1823,7 @@ bitflags! {
const DATACLASSES = 1 << 17; const DATACLASSES = 1 << 17;
const BUILTINS = 1 << 18; const BUILTINS = 1 << 18;
const CONTEXTVARS = 1 << 19; const CONTEXTVARS = 1 << 19;
const ANYIO = 1 << 20;
} }
} }