mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-09 13:18:52 +00:00
[ty] Add hint if async context manager is used in non-async with statement (#18299)
# Summary Adds a subdiagnostic hint in the following scenario where a synchronous `with` is used with an async context manager: ```py class Manager: async def __aenter__(self): ... async def __aexit__(self, *args): ... # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`" # note: Objects of type `Manager` *can* be used as async context managers # note: Consider using `async with` here with Manager(): ... ``` closes https://github.com/astral-sh/ty/issues/508 ## Test Plan New MD snapshot tests --------- Co-authored-by: David Peter <mail@david-peter.de>
This commit is contained in:
parent
62ef96f51e
commit
1d20cf9570
3 changed files with 147 additions and 1 deletions
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: sync.md - With statements - Accidental use of non-async `with`
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/with/sync.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class Manager:
|
||||
2 | async def __aenter__(self): ...
|
||||
3 | async def __aexit__(self, *args): ...
|
||||
4 |
|
||||
5 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`"
|
||||
6 | with Manager():
|
||||
7 | ...
|
||||
8 | class Manager:
|
||||
9 | async def __aenter__(self): ...
|
||||
10 | async def __aexit__(self, typ: str, exc, traceback): ...
|
||||
11 |
|
||||
12 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`"
|
||||
13 | with Manager():
|
||||
14 | ...
|
||||
15 | class Manager:
|
||||
16 | async def __aenter__(self, wrong_extra_arg): ...
|
||||
17 | async def __aexit__(self, typ, exc, traceback, wrong_extra_arg): ...
|
||||
18 |
|
||||
19 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`"
|
||||
20 | with Manager():
|
||||
21 | ...
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-context-manager]: Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`
|
||||
--> src/mdtest_snippet.py:6:6
|
||||
|
|
||||
5 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and...
|
||||
6 | with Manager():
|
||||
| ^^^^^^^^^
|
||||
7 | ...
|
||||
8 | class Manager:
|
||||
|
|
||||
info: Objects of type `Manager` can be used as async context managers
|
||||
info: Consider using `async with` here
|
||||
info: rule `invalid-context-manager` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-context-manager]: Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`
|
||||
--> src/mdtest_snippet.py:13:6
|
||||
|
|
||||
12 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` an...
|
||||
13 | with Manager():
|
||||
| ^^^^^^^^^
|
||||
14 | ...
|
||||
15 | class Manager:
|
||||
|
|
||||
info: Objects of type `Manager` can be used as async context managers
|
||||
info: Consider using `async with` here
|
||||
info: rule `invalid-context-manager` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-context-manager]: Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`
|
||||
--> src/mdtest_snippet.py:20:6
|
||||
|
|
||||
19 | # error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` an...
|
||||
20 | with Manager():
|
||||
| ^^^^^^^^^
|
||||
21 | ...
|
||||
|
|
||||
info: Objects of type `Manager` can be used as async context managers
|
||||
info: Consider using `async with` here
|
||||
info: rule `invalid-context-manager` is enabled by default
|
||||
|
||||
```
|
|
@ -149,3 +149,45 @@ context_expr = Manager()
|
|||
with context_expr as f:
|
||||
reveal_type(f) # revealed: str
|
||||
```
|
||||
|
||||
## Accidental use of non-async `with`
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
If a synchronous `with` statement is used on a type with `__aenter__` and `__aexit__`, we show a
|
||||
diagnostic hint that the user might have intended to use `asnyc with` instead.
|
||||
|
||||
```py
|
||||
class Manager:
|
||||
async def __aenter__(self): ...
|
||||
async def __aexit__(self, *args): ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
|
||||
The sub-diagnostic is also provided if the signatures of `__aenter__` and `__aexit__` do not match
|
||||
the expected signatures for a context manager:
|
||||
|
||||
```py
|
||||
class Manager:
|
||||
async def __aenter__(self): ...
|
||||
async def __aexit__(self, typ: str, exc, traceback): ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
|
||||
Similarly, we also show the hint if the functions have the wrong number of arguments:
|
||||
|
||||
```py
|
||||
class Manager:
|
||||
async def __aenter__(self, wrong_extra_arg): ...
|
||||
async def __aexit__(self, typ, exc, traceback, wrong_extra_arg): ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not implement `__enter__` and `__exit__`"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
|
|
|
@ -6133,12 +6133,31 @@ impl<'db> ContextManagerError<'db> {
|
|||
} => format_call_dunder_errors(enter_error, "__enter__", exit_error, "__exit__"),
|
||||
};
|
||||
|
||||
builder.into_diagnostic(
|
||||
let mut diag = builder.into_diagnostic(
|
||||
format_args!(
|
||||
"Object of type `{context_expression}` cannot be used with `with` because {formatted_errors}",
|
||||
context_expression = context_expression_type.display(db)
|
||||
),
|
||||
);
|
||||
|
||||
// If `__aenter__` and `__aexit__` are available, the user may have intended to use `async with` instead of `with`:
|
||||
if let (
|
||||
Ok(_) | Err(CallDunderError::CallError(..)),
|
||||
Ok(_) | Err(CallDunderError::CallError(..)),
|
||||
) = (
|
||||
context_expression_type.try_call_dunder(db, "__aenter__", CallArgumentTypes::none()),
|
||||
context_expression_type.try_call_dunder(
|
||||
db,
|
||||
"__aexit__",
|
||||
CallArgumentTypes::positional([Type::unknown(), Type::unknown(), Type::unknown()]),
|
||||
),
|
||||
) {
|
||||
diag.info(format_args!(
|
||||
"Objects of type `{context_expression}` can be used as async context managers",
|
||||
context_expression = context_expression_type.display(db)
|
||||
));
|
||||
diag.info("Consider using `async with` here");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue