-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[red-knot] Handle context managers in (sync) with statements #13998
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# Async with declarations | ||
|
||
## Basic `async with` statement | ||
|
||
The type of the target variable in a `with` statement should be the return type from the context manager's `__aenter__` method. | ||
However, Knot doesn't support `async with` statements yet. This test asserts that it doesn't emit any context manager-related errors. | ||
|
||
```py | ||
class Target: ... | ||
|
||
class Manager: | ||
async def __aenter__(self) -> Target: | ||
return Target() | ||
|
||
async def __aexit__(self, exc_type, exc_value, traceback): ... | ||
|
||
async def test(): | ||
async with Manager() as f: | ||
reveal_type(f) # revealed: @Todo | ||
``` |
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,140 @@ | ||||||||
# With declarations | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit about this title, and the location of this file: with statements create Definitions, which are Bindings, but are not Declarations. (A declaration is only an annotated assignment, or an annotated function parameter, or a function or class definition.) So I think this title should be just Regarding file location, we don't have a general directory for all kinds of definitions/bindings, so I think |
||||||||
|
||||||||
## Basic `with` statement | ||||||||
|
||||||||
The type of the target variable in a `with` statement is the return type from the context manager's `__enter__` method. | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
|
||||||||
```py | ||||||||
class Target: ... | ||||||||
|
||||||||
class Manager: | ||||||||
def __enter__(self) -> Target: | ||||||||
return Target() | ||||||||
|
||||||||
def __exit__(self, exc_type, exc_value, traceback): ... | ||||||||
|
||||||||
with Manager() as f: | ||||||||
reveal_type(f) # revealed: Target | ||||||||
``` | ||||||||
|
||||||||
## Union context manager | ||||||||
|
||||||||
```py | ||||||||
def coinflip() -> bool: | ||||||||
return True | ||||||||
|
||||||||
class Manager1: | ||||||||
def __enter__(self) -> str: | ||||||||
return "foo" | ||||||||
|
||||||||
def __exit__(self, exc_type, exc_value, traceback): ... | ||||||||
|
||||||||
class Manager2: | ||||||||
def __enter__(self) -> int: | ||||||||
return 42 | ||||||||
|
||||||||
def __exit__(self, exc_type, exc_value, traceback): ... | ||||||||
|
||||||||
context_expr = Manager1() if coinflip() else Manager2() | ||||||||
|
||||||||
with context_expr as f: | ||||||||
reveal_type(f) # revealed: str | int | ||||||||
``` | ||||||||
|
||||||||
## Context manager without an `__enter__` or `__exit__` method | ||||||||
|
||||||||
```py | ||||||||
class Manager: ... | ||||||||
|
||||||||
# error: [invalid-context-manager] "Object of type Manager cannot be used with `with` because it doesn't implement `__enter__` and `__exit__`" | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The diagnostic messages here and below are excellent! Super clear and informative. |
||||||||
with Manager(): | ||||||||
... | ||||||||
``` | ||||||||
|
||||||||
## Context manager without an `__enter__` method | ||||||||
|
||||||||
```py | ||||||||
class Manager: | ||||||||
def __exit__(self, exc_tpe, exc_value, traceback): ... | ||||||||
|
||||||||
# error: [invalid-context-manager] "Object of type Manager cannot be used with `with` because it doesn't implement `__enter__`" | ||||||||
with Manager(): | ||||||||
... | ||||||||
``` | ||||||||
|
||||||||
## Context manager without an `__exit__` method | ||||||||
|
||||||||
```py | ||||||||
class Manager: | ||||||||
def __enter__(self): ... | ||||||||
|
||||||||
# error: [invalid-context-manager] "Object of type Manager cannot be used with `with` because it doesn't implement `__exit__`" | ||||||||
with Manager(): | ||||||||
... | ||||||||
``` | ||||||||
|
||||||||
## Context manager with non-callable `__enter__` attribute | ||||||||
|
||||||||
```py | ||||||||
class Manager: | ||||||||
__enter__ = 42 | ||||||||
|
||||||||
def __exit__(self, exc_tpe, exc_value, traceback): ... | ||||||||
|
||||||||
# error: [invalid-context-manager] "Object of type Manager cannot be used with `with` because the method `__enter__` of type Literal[42] is not callable" | ||||||||
with Manager(): | ||||||||
... | ||||||||
``` | ||||||||
|
||||||||
## Context manager with non-callable `__exit__` attribute | ||||||||
|
||||||||
```py | ||||||||
class Manager: | ||||||||
def __enter__(self) -> Self: ... | ||||||||
|
||||||||
__exit__ = 32 | ||||||||
|
||||||||
# error: [invalid-context-manager] "Object of type Manager cannot be used with `with` because the method `__exit__` of type Literal[32] is not callable" | ||||||||
with Manager(): | ||||||||
... | ||||||||
``` | ||||||||
|
||||||||
## Context expression with non-callable union variants | ||||||||
|
||||||||
```py | ||||||||
def coinflip() -> bool: | ||||||||
return True | ||||||||
|
||||||||
class Manager1: | ||||||||
def __enter__(self) -> str: | ||||||||
return "foo" | ||||||||
|
||||||||
def __exit__(self, exc_type, exc_value, traceback): ... | ||||||||
|
||||||||
class NotAContextManager: ... | ||||||||
|
||||||||
context_expr = Manager1() if coinflip() else NotAContextManager() | ||||||||
|
||||||||
# error: [invalid-context-manager] "Object of type Manager1 | NotAContextManager cannot be used with `with` because the method `__enter__` of type Literal[__enter__] | Unbound is not callable" | ||||||||
# error: [invalid-context-manager] "Object of type Manager1 | NotAContextManager cannot be used with `with` because the method `__exit__` of type Literal[__exit__] | Unbound is not callable" | ||||||||
with context_expr as f: | ||||||||
reveal_type(f) # revealed: str | Unknown | ||||||||
``` | ||||||||
|
||||||||
## Context expression with "sometimes" callable `__enter__` method | ||||||||
|
||||||||
```py | ||||||||
def coinflip() -> bool: | ||||||||
return True | ||||||||
|
||||||||
class Manager: | ||||||||
if coinflip(): | ||||||||
def __enter__(self) -> str: | ||||||||
return "abcd" | ||||||||
|
||||||||
def __exit__(self, *args): ... | ||||||||
|
||||||||
with Manager() as f: | ||||||||
# TODO: This should emit an error that `__enter__` is possibly unbound. | ||||||||
reveal_type(f) # revealed: str | ||||||||
``` |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -1010,8 +1010,6 @@ impl<'db> Type<'db> { | |||||
} | ||||||
|
||||||
/// Return the type resulting from calling an object of this type. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While we're editing this comment anyway, this part isn't accurate anymore either. I think it could be edited as below. Arguably that doesn't give much information that isn't obvious, but I think it's not 100% obvious that We could also do a much longer doc comment here or elsewhere that talks about how to actually correctly use
Suggested change
|
||||||
/// | ||||||
/// Returns `None` if `self` is not a callable type. | ||||||
MichaReiser marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
#[must_use] | ||||||
fn call(self, db: &'db dyn Db, arg_types: &[Type<'db>]) -> CallOutcome<'db> { | ||||||
match self { | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: quite long lines here. Also, we haven't renamed it to
Knot
yet ;)