[pygrep_hooks] Add AsyncMock methods to invalid-mock-access (PGH005) (#18547)

## Summary
This PR expands PGH005 to also check for AsyncMock methods in the same
vein. E.g., currently `assert mock.not_called` is linted. This PR adds
the corresponding async assertions `assert mock.not_awaited()`.

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
This commit is contained in:
Robsdedude 2025-06-24 21:27:21 +00:00 committed by GitHub
parent 9d8cba4e8b
commit 919af9628d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 339 additions and 73 deletions

View file

@ -1,3 +1,5 @@
# Mock objects
# ============
# Errors
assert my_mock.not_called()
assert my_mock.called_once_with()
@ -17,3 +19,25 @@ my_mock.assert_called()
my_mock.assert_called_once_with()
"""like :meth:`Mock.assert_called_once_with`"""
"""like :meth:`MagicMock.assert_called_once_with`"""
# AsyncMock objects
# =================
# Errors
assert my_mock.not_awaited()
assert my_mock.awaited_once_with()
assert my_mock.not_awaited
assert my_mock.awaited_once_with
my_mock.assert_not_awaited
my_mock.assert_awaited
my_mock.assert_awaited_once_with
my_mock.assert_awaited_once_with
MyMock.assert_awaited_once_with
assert my_mock.awaited
# OK
assert my_mock.await_count == 1
my_mock.assert_not_awaited()
my_mock.assert_awaited()
my_mock.assert_awaited_once_with()
"""like :meth:`Mock.assert_awaited_once_with`"""
"""like :meth:`MagicMock.assert_awaited_once_with`"""

View file

@ -89,3 +89,8 @@ pub(crate) const fn is_ignore_init_files_in_useless_alias_enabled(
) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/18547
pub(crate) const fn is_invalid_async_mock_access_check_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View file

@ -10,6 +10,7 @@ mod tests {
use crate::registry::Rule;
use crate::settings::types::PreviewMode;
use crate::test::test_path;
use crate::{assert_diagnostics, settings};
@ -29,4 +30,22 @@ mod tests {
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::InvalidMockAccess, Path::new("PGH005_0.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("pygrep_hooks").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
}

View file

@ -5,6 +5,7 @@ use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::preview::is_invalid_async_mock_access_check_enabled;
#[derive(Debug, PartialEq, Eq)]
enum Reason {
@ -51,7 +52,7 @@ impl Violation for InvalidMockAccess {
/// PGH005
pub(crate) fn uncalled_mock_method(checker: &Checker, expr: &Expr) {
if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = expr {
if matches!(
let is_uncalled_mock_method = matches!(
attr.as_str(),
"assert_any_call"
| "assert_called"
@ -60,7 +61,20 @@ pub(crate) fn uncalled_mock_method(checker: &Checker, expr: &Expr) {
| "assert_called_with"
| "assert_has_calls"
| "assert_not_called"
) {
);
let is_uncalled_async_mock_method =
is_invalid_async_mock_access_check_enabled(checker.settings())
&& matches!(
attr.as_str(),
"assert_awaited"
| "assert_awaited_once"
| "assert_awaited_with"
| "assert_awaited_once_with"
| "assert_any_await"
| "assert_has_awaits"
| "assert_not_awaited"
);
if is_uncalled_mock_method || is_uncalled_async_mock_method {
checker.report_diagnostic(
InvalidMockAccess {
reason: Reason::UncalledMethod(attr.to_string()),
@ -81,7 +95,7 @@ pub(crate) fn non_existent_mock_method(checker: &Checker, test: &Expr) {
},
_ => return,
};
if matches!(
let is_missing_mock_method = matches!(
attr.as_str(),
"any_call"
| "called_once"
@ -89,7 +103,20 @@ pub(crate) fn non_existent_mock_method(checker: &Checker, test: &Expr) {
| "called_with"
| "has_calls"
| "not_called"
) {
);
let is_missing_async_mock_method =
is_invalid_async_mock_access_check_enabled(checker.settings())
&& matches!(
attr.as_str(),
"awaited"
| "awaited_once"
| "awaited_with"
| "awaited_once_with"
| "any_await"
| "has_awaits"
| "not_awaited"
);
if is_missing_mock_method || is_missing_async_mock_method {
checker.report_diagnostic(
InvalidMockAccess {
reason: Reason::NonExistentMethod(attr.to_string()),

View file

@ -1,90 +1,91 @@
---
source: crates/ruff_linter/src/rules/pygrep_hooks/mod.rs
---
PGH005_0.py:2:8: PGH005 Non-existent mock method: `not_called`
|
1 | # Errors
2 | assert my_mock.not_called()
| ^^^^^^^^^^^^^^^^^^^^ PGH005
3 | assert my_mock.called_once_with()
4 | assert my_mock.not_called
|
PGH005_0.py:3:8: PGH005 Non-existent mock method: `called_once_with`
|
1 | # Errors
2 | assert my_mock.not_called()
3 | assert my_mock.called_once_with()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
4 | assert my_mock.not_called
5 | assert my_mock.called_once_with
|
PGH005_0.py:4:8: PGH005 Non-existent mock method: `not_called`
|
2 | assert my_mock.not_called()
3 | assert my_mock.called_once_with()
4 | assert my_mock.not_called
| ^^^^^^^^^^^^^^^^^^ PGH005
5 | assert my_mock.called_once_with
6 | my_mock.assert_not_called
2 | # ============
3 | # Errors
4 | assert my_mock.not_called()
| ^^^^^^^^^^^^^^^^^^^^ PGH005
5 | assert my_mock.called_once_with()
6 | assert my_mock.not_called
|
PGH005_0.py:5:8: PGH005 Non-existent mock method: `called_once_with`
|
3 | assert my_mock.called_once_with()
4 | assert my_mock.not_called
5 | assert my_mock.called_once_with
3 | # Errors
4 | assert my_mock.not_called()
5 | assert my_mock.called_once_with()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
6 | assert my_mock.not_called
7 | assert my_mock.called_once_with
|
PGH005_0.py:6:8: PGH005 Non-existent mock method: `not_called`
|
4 | assert my_mock.not_called()
5 | assert my_mock.called_once_with()
6 | assert my_mock.not_called
| ^^^^^^^^^^^^^^^^^^ PGH005
7 | assert my_mock.called_once_with
8 | my_mock.assert_not_called
|
PGH005_0.py:7:8: PGH005 Non-existent mock method: `called_once_with`
|
5 | assert my_mock.called_once_with()
6 | assert my_mock.not_called
7 | assert my_mock.called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
6 | my_mock.assert_not_called
7 | my_mock.assert_called
8 | my_mock.assert_not_called
9 | my_mock.assert_called
|
PGH005_0.py:6:1: PGH005 Mock method should be called: `assert_not_called`
|
4 | assert my_mock.not_called
5 | assert my_mock.called_once_with
6 | my_mock.assert_not_called
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
7 | my_mock.assert_called
8 | my_mock.assert_called_once_with
|
PGH005_0.py:7:1: PGH005 Mock method should be called: `assert_called`
|
5 | assert my_mock.called_once_with
6 | my_mock.assert_not_called
7 | my_mock.assert_called
| ^^^^^^^^^^^^^^^^^^^^^ PGH005
8 | my_mock.assert_called_once_with
9 | my_mock.assert_called_once_with
|
PGH005_0.py:8:1: PGH005 Mock method should be called: `assert_called_once_with`
PGH005_0.py:8:1: PGH005 Mock method should be called: `assert_not_called`
|
6 | my_mock.assert_not_called
7 | my_mock.assert_called
8 | my_mock.assert_called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
9 | my_mock.assert_called_once_with
10 | MyMock.assert_called_once_with
6 | assert my_mock.not_called
7 | assert my_mock.called_once_with
8 | my_mock.assert_not_called
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
9 | my_mock.assert_called
10 | my_mock.assert_called_once_with
|
PGH005_0.py:9:1: PGH005 Mock method should be called: `assert_called_once_with`
PGH005_0.py:9:1: PGH005 Mock method should be called: `assert_called`
|
7 | my_mock.assert_called
8 | my_mock.assert_called_once_with
9 | my_mock.assert_called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
10 | MyMock.assert_called_once_with
7 | assert my_mock.called_once_with
8 | my_mock.assert_not_called
9 | my_mock.assert_called
| ^^^^^^^^^^^^^^^^^^^^^ PGH005
10 | my_mock.assert_called_once_with
11 | my_mock.assert_called_once_with
|
PGH005_0.py:10:1: PGH005 Mock method should be called: `assert_called_once_with`
|
8 | my_mock.assert_called_once_with
9 | my_mock.assert_called_once_with
10 | MyMock.assert_called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
11 |
12 | # OK
8 | my_mock.assert_not_called
9 | my_mock.assert_called
10 | my_mock.assert_called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
11 | my_mock.assert_called_once_with
12 | MyMock.assert_called_once_with
|
PGH005_0.py:11:1: PGH005 Mock method should be called: `assert_called_once_with`
|
9 | my_mock.assert_called
10 | my_mock.assert_called_once_with
11 | my_mock.assert_called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
12 | MyMock.assert_called_once_with
|
PGH005_0.py:12:1: PGH005 Mock method should be called: `assert_called_once_with`
|
10 | my_mock.assert_called_once_with
11 | my_mock.assert_called_once_with
12 | MyMock.assert_called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
13 |
14 | # OK
|

View file

@ -0,0 +1,190 @@
---
source: crates/ruff_linter/src/rules/pygrep_hooks/mod.rs
---
PGH005_0.py:4:8: PGH005 Non-existent mock method: `not_called`
|
2 | # ============
3 | # Errors
4 | assert my_mock.not_called()
| ^^^^^^^^^^^^^^^^^^^^ PGH005
5 | assert my_mock.called_once_with()
6 | assert my_mock.not_called
|
PGH005_0.py:5:8: PGH005 Non-existent mock method: `called_once_with`
|
3 | # Errors
4 | assert my_mock.not_called()
5 | assert my_mock.called_once_with()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
6 | assert my_mock.not_called
7 | assert my_mock.called_once_with
|
PGH005_0.py:6:8: PGH005 Non-existent mock method: `not_called`
|
4 | assert my_mock.not_called()
5 | assert my_mock.called_once_with()
6 | assert my_mock.not_called
| ^^^^^^^^^^^^^^^^^^ PGH005
7 | assert my_mock.called_once_with
8 | my_mock.assert_not_called
|
PGH005_0.py:7:8: PGH005 Non-existent mock method: `called_once_with`
|
5 | assert my_mock.called_once_with()
6 | assert my_mock.not_called
7 | assert my_mock.called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
8 | my_mock.assert_not_called
9 | my_mock.assert_called
|
PGH005_0.py:8:1: PGH005 Mock method should be called: `assert_not_called`
|
6 | assert my_mock.not_called
7 | assert my_mock.called_once_with
8 | my_mock.assert_not_called
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
9 | my_mock.assert_called
10 | my_mock.assert_called_once_with
|
PGH005_0.py:9:1: PGH005 Mock method should be called: `assert_called`
|
7 | assert my_mock.called_once_with
8 | my_mock.assert_not_called
9 | my_mock.assert_called
| ^^^^^^^^^^^^^^^^^^^^^ PGH005
10 | my_mock.assert_called_once_with
11 | my_mock.assert_called_once_with
|
PGH005_0.py:10:1: PGH005 Mock method should be called: `assert_called_once_with`
|
8 | my_mock.assert_not_called
9 | my_mock.assert_called
10 | my_mock.assert_called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
11 | my_mock.assert_called_once_with
12 | MyMock.assert_called_once_with
|
PGH005_0.py:11:1: PGH005 Mock method should be called: `assert_called_once_with`
|
9 | my_mock.assert_called
10 | my_mock.assert_called_once_with
11 | my_mock.assert_called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
12 | MyMock.assert_called_once_with
|
PGH005_0.py:12:1: PGH005 Mock method should be called: `assert_called_once_with`
|
10 | my_mock.assert_called_once_with
11 | my_mock.assert_called_once_with
12 | MyMock.assert_called_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
13 |
14 | # OK
|
PGH005_0.py:26:8: PGH005 Non-existent mock method: `not_awaited`
|
24 | # =================
25 | # Errors
26 | assert my_mock.not_awaited()
| ^^^^^^^^^^^^^^^^^^^^^ PGH005
27 | assert my_mock.awaited_once_with()
28 | assert my_mock.not_awaited
|
PGH005_0.py:27:8: PGH005 Non-existent mock method: `awaited_once_with`
|
25 | # Errors
26 | assert my_mock.not_awaited()
27 | assert my_mock.awaited_once_with()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
28 | assert my_mock.not_awaited
29 | assert my_mock.awaited_once_with
|
PGH005_0.py:28:8: PGH005 Non-existent mock method: `not_awaited`
|
26 | assert my_mock.not_awaited()
27 | assert my_mock.awaited_once_with()
28 | assert my_mock.not_awaited
| ^^^^^^^^^^^^^^^^^^^ PGH005
29 | assert my_mock.awaited_once_with
30 | my_mock.assert_not_awaited
|
PGH005_0.py:29:8: PGH005 Non-existent mock method: `awaited_once_with`
|
27 | assert my_mock.awaited_once_with()
28 | assert my_mock.not_awaited
29 | assert my_mock.awaited_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
30 | my_mock.assert_not_awaited
31 | my_mock.assert_awaited
|
PGH005_0.py:30:1: PGH005 Mock method should be called: `assert_not_awaited`
|
28 | assert my_mock.not_awaited
29 | assert my_mock.awaited_once_with
30 | my_mock.assert_not_awaited
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
31 | my_mock.assert_awaited
32 | my_mock.assert_awaited_once_with
|
PGH005_0.py:31:1: PGH005 Mock method should be called: `assert_awaited`
|
29 | assert my_mock.awaited_once_with
30 | my_mock.assert_not_awaited
31 | my_mock.assert_awaited
| ^^^^^^^^^^^^^^^^^^^^^^ PGH005
32 | my_mock.assert_awaited_once_with
33 | my_mock.assert_awaited_once_with
|
PGH005_0.py:32:1: PGH005 Mock method should be called: `assert_awaited_once_with`
|
30 | my_mock.assert_not_awaited
31 | my_mock.assert_awaited
32 | my_mock.assert_awaited_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
33 | my_mock.assert_awaited_once_with
34 | MyMock.assert_awaited_once_with
|
PGH005_0.py:33:1: PGH005 Mock method should be called: `assert_awaited_once_with`
|
31 | my_mock.assert_awaited
32 | my_mock.assert_awaited_once_with
33 | my_mock.assert_awaited_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
34 | MyMock.assert_awaited_once_with
35 | assert my_mock.awaited
|
PGH005_0.py:34:1: PGH005 Mock method should be called: `assert_awaited_once_with`
|
32 | my_mock.assert_awaited_once_with
33 | my_mock.assert_awaited_once_with
34 | MyMock.assert_awaited_once_with
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PGH005
35 | assert my_mock.awaited
|
PGH005_0.py:35:8: PGH005 Non-existent mock method: `awaited`
|
33 | my_mock.assert_awaited_once_with
34 | MyMock.assert_awaited_once_with
35 | assert my_mock.awaited
| ^^^^^^^^^^^^^^^ PGH005
36 |
37 | # OK
|