mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 05:44:56 +00:00
[ty] Support async
/await
, async with
and yield from
(#19595)
## Summary - Add support for the return types of `async` functions - Add type inference for `await` expressions - Add support for `async with` / async context managers - Add support for `yield from` expressions This PR is generally lacking proper error handling in some cases (e.g. illegal `__await__` attributes). I'm planning to work on this in a follow-up. part of https://github.com/astral-sh/ty/issues/151 closes https://github.com/astral-sh/ty/issues/736 ## Ecosystem There are a lot of true positives on `prefect` which look similar to: ```diff prefect (https://github.com/PrefectHQ/prefect) + src/integrations/prefect-aws/tests/workers/test_ecs_worker.py:406:12: error[unresolved-attribute] Type `str` has no attribute `status_code` ``` This is due to a wrong return type annotation [here](e926b8c4c1/src/integrations/prefect-aws/tests/workers/test_ecs_worker.py (L355-L391)
). ```diff mitmproxy (https://github.com/mitmproxy/mitmproxy) + test/mitmproxy/addons/test_clientplayback.py:18:1: error[invalid-argument-type] Argument to function `asynccontextmanager` is incorrect: Expected `(...) -> AsyncIterator[Unknown]`, found `def tcp_server(handle_conn, **server_args) -> Unknown | tuple[str, int]` ``` [This](a4d794c59a/test/mitmproxy/addons/test_clientplayback.py (L18-L19)
) is a true positive. That function should return `AsyncIterator[Address]`, not `Address`. I looked through almost all of the other new diagnostics and they all look like known problems or true positives. ## Typing conformance The typing conformance diff looks good. ## Test Plan New Markdown tests
This commit is contained in:
parent
c5ac998892
commit
4ecf1d205a
12 changed files with 472 additions and 46 deletions
|
@ -2805,7 +2805,9 @@ impl<'ast> Unpackable<'ast> {
|
|||
match self {
|
||||
Unpackable::Assign(_) => UnpackKind::Assign,
|
||||
Unpackable::For(_) | Unpackable::Comprehension { .. } => UnpackKind::Iterable,
|
||||
Unpackable::WithItem { .. } => UnpackKind::ContextManager,
|
||||
Unpackable::WithItem { is_async, .. } => UnpackKind::ContextManager {
|
||||
is_async: *is_async,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4790,6 +4790,64 @@ impl<'db> Type<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Similar to [`Self::try_enter`], but for async context managers.
|
||||
fn aenter(self, db: &'db dyn Db) -> Type<'db> {
|
||||
// TODO: Add proper error handling and rename this method to `try_aenter`.
|
||||
self.try_call_dunder(db, "__aenter__", CallArguments::none())
|
||||
.map_or(Type::unknown(), |result| {
|
||||
result.return_type(db).resolve_await(db)
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolve the type of an `await …` expression where `self` is the type of the awaitable.
|
||||
fn resolve_await(self, db: &'db dyn Db) -> Type<'db> {
|
||||
// TODO: Add proper error handling and rename this method to `try_await`.
|
||||
self.try_call_dunder(db, "__await__", CallArguments::none())
|
||||
.map_or(Type::unknown(), |result| {
|
||||
result
|
||||
.return_type(db)
|
||||
.generator_return_type(db)
|
||||
.unwrap_or_else(Type::unknown)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the return type of a `yield from …` expression where `self` is the type of the generator.
|
||||
///
|
||||
/// This corresponds to the `ReturnT` parameter of the generic `typing.Generator[YieldT, SendT, ReturnT]`
|
||||
/// protocol.
|
||||
fn generator_return_type(self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
// TODO: Ideally, we would first try to upcast `self` to an instance of `Generator` and *then*
|
||||
// match on the protocol instance to get the `ReturnType` type parameter. For now, implement
|
||||
// an ad-hoc solution that works for protocols and instances of classes that directly inherit
|
||||
// from the `Generator` protocol, such as `types.GeneratorType`.
|
||||
|
||||
let from_class_base = |base: ClassBase<'db>| {
|
||||
let class = base.into_class()?;
|
||||
if class.is_known(db, KnownClass::Generator) {
|
||||
if let Some(specialization) = class.class_literal_specialized(db, None).1 {
|
||||
if let [_, _, return_ty] = specialization.types(db) {
|
||||
return Some(*return_ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
};
|
||||
|
||||
match self {
|
||||
Type::NominalInstance(instance) => {
|
||||
instance.class.iter_mro(db).find_map(from_class_base)
|
||||
}
|
||||
Type::ProtocolInstance(instance) => {
|
||||
if let Protocol::FromClass(class) = instance.inner {
|
||||
class.iter_mro(db).find_map(from_class_base)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a class literal or non-dynamic SubclassOf type, try calling it (creating an instance)
|
||||
/// and return the resulting instance type.
|
||||
///
|
||||
|
|
|
@ -2614,10 +2614,13 @@ pub enum KnownClass {
|
|||
UnionType,
|
||||
GeneratorType,
|
||||
AsyncGeneratorType,
|
||||
CoroutineType,
|
||||
// Typeshed
|
||||
NoneType, // Part of `types` for Python >= 3.10
|
||||
// Typing
|
||||
Any,
|
||||
Awaitable,
|
||||
Generator,
|
||||
Deprecated,
|
||||
StdlibAlias,
|
||||
SpecialForm,
|
||||
|
@ -2689,7 +2692,8 @@ impl KnownClass {
|
|||
| Self::UnionType
|
||||
| Self::GeneratorType
|
||||
| Self::AsyncGeneratorType
|
||||
| Self::MethodWrapperType => Truthiness::AlwaysTrue,
|
||||
| Self::MethodWrapperType
|
||||
| Self::CoroutineType => Truthiness::AlwaysTrue,
|
||||
|
||||
Self::NoneType => Truthiness::AlwaysFalse,
|
||||
|
||||
|
@ -2740,6 +2744,8 @@ impl KnownClass {
|
|||
| Self::NotImplementedType
|
||||
| Self::Staticmethod
|
||||
| Self::Classmethod
|
||||
| Self::Awaitable
|
||||
| Self::Generator
|
||||
| Self::Deprecated
|
||||
| Self::Field
|
||||
| Self::KwOnly
|
||||
|
@ -2805,12 +2811,15 @@ impl KnownClass {
|
|||
| Self::InitVar
|
||||
| Self::VersionInfo
|
||||
| Self::Bool
|
||||
| Self::NoneType => false,
|
||||
| Self::NoneType
|
||||
| Self::CoroutineType => false,
|
||||
|
||||
// Anything with a *runtime* MRO (N.B. sometimes different from the MRO that typeshed gives!)
|
||||
// with length >2, or anything that is implemented in pure Python, is not a solid base.
|
||||
Self::ABCMeta
|
||||
| Self::Any
|
||||
| Self::Awaitable
|
||||
| Self::Generator
|
||||
| Self::Enum
|
||||
| Self::EnumType
|
||||
| Self::Auto
|
||||
|
@ -2859,6 +2868,8 @@ impl KnownClass {
|
|||
| KnownClass::ExceptionGroup
|
||||
| KnownClass::Staticmethod
|
||||
| KnownClass::Classmethod
|
||||
| KnownClass::Awaitable
|
||||
| KnownClass::Generator
|
||||
| KnownClass::Deprecated
|
||||
| KnownClass::Super
|
||||
| KnownClass::Enum
|
||||
|
@ -2876,6 +2887,7 @@ impl KnownClass {
|
|||
| KnownClass::UnionType
|
||||
| KnownClass::GeneratorType
|
||||
| KnownClass::AsyncGeneratorType
|
||||
| KnownClass::CoroutineType
|
||||
| KnownClass::NoneType
|
||||
| KnownClass::Any
|
||||
| KnownClass::StdlibAlias
|
||||
|
@ -2921,7 +2933,11 @@ impl KnownClass {
|
|||
/// 2. It's probably more performant.
|
||||
const fn is_protocol(self) -> bool {
|
||||
match self {
|
||||
Self::SupportsIndex | Self::Iterable | Self::Iterator => true,
|
||||
Self::SupportsIndex
|
||||
| Self::Iterable
|
||||
| Self::Iterator
|
||||
| Self::Awaitable
|
||||
| Self::Generator => true,
|
||||
|
||||
Self::Any
|
||||
| Self::Bool
|
||||
|
@ -2950,6 +2966,7 @@ impl KnownClass {
|
|||
| Self::GenericAlias
|
||||
| Self::GeneratorType
|
||||
| Self::AsyncGeneratorType
|
||||
| Self::CoroutineType
|
||||
| Self::ModuleType
|
||||
| Self::FunctionType
|
||||
| Self::MethodType
|
||||
|
@ -3015,6 +3032,8 @@ impl KnownClass {
|
|||
Self::ExceptionGroup => "ExceptionGroup",
|
||||
Self::Staticmethod => "staticmethod",
|
||||
Self::Classmethod => "classmethod",
|
||||
Self::Awaitable => "Awaitable",
|
||||
Self::Generator => "Generator",
|
||||
Self::Deprecated => "deprecated",
|
||||
Self::GenericAlias => "GenericAlias",
|
||||
Self::ModuleType => "ModuleType",
|
||||
|
@ -3025,6 +3044,7 @@ impl KnownClass {
|
|||
Self::WrapperDescriptorType => "WrapperDescriptorType",
|
||||
Self::GeneratorType => "GeneratorType",
|
||||
Self::AsyncGeneratorType => "AsyncGeneratorType",
|
||||
Self::CoroutineType => "CoroutineType",
|
||||
Self::NamedTuple => "NamedTuple",
|
||||
Self::NoneType => "NoneType",
|
||||
Self::SpecialForm => "_SpecialForm",
|
||||
|
@ -3285,11 +3305,14 @@ impl KnownClass {
|
|||
| Self::MethodType
|
||||
| Self::GeneratorType
|
||||
| Self::AsyncGeneratorType
|
||||
| Self::CoroutineType
|
||||
| Self::MethodWrapperType
|
||||
| Self::UnionType
|
||||
| Self::WrapperDescriptorType => KnownModule::Types,
|
||||
Self::NoneType => KnownModule::Typeshed,
|
||||
Self::Any
|
||||
| Self::Awaitable
|
||||
| Self::Generator
|
||||
| Self::SpecialForm
|
||||
| Self::TypeVar
|
||||
| Self::NamedTuple
|
||||
|
@ -3370,12 +3393,15 @@ impl KnownClass {
|
|||
| Self::ExceptionGroup
|
||||
| Self::Staticmethod
|
||||
| Self::Classmethod
|
||||
| Self::Awaitable
|
||||
| Self::Generator
|
||||
| Self::Deprecated
|
||||
| Self::GenericAlias
|
||||
| Self::ModuleType
|
||||
| Self::FunctionType
|
||||
| Self::GeneratorType
|
||||
| Self::AsyncGeneratorType
|
||||
| Self::CoroutineType
|
||||
| Self::MethodType
|
||||
| Self::MethodWrapperType
|
||||
| Self::WrapperDescriptorType
|
||||
|
@ -3447,6 +3473,7 @@ impl KnownClass {
|
|||
| Self::WrapperDescriptorType
|
||||
| Self::GeneratorType
|
||||
| Self::AsyncGeneratorType
|
||||
| Self::CoroutineType
|
||||
| Self::SpecialForm
|
||||
| Self::ChainMap
|
||||
| Self::Counter
|
||||
|
@ -3461,6 +3488,8 @@ impl KnownClass {
|
|||
| Self::ExceptionGroup
|
||||
| Self::Staticmethod
|
||||
| Self::Classmethod
|
||||
| Self::Awaitable
|
||||
| Self::Generator
|
||||
| Self::Deprecated
|
||||
| Self::TypeVar
|
||||
| Self::ParamSpec
|
||||
|
@ -3517,12 +3546,15 @@ impl KnownClass {
|
|||
"ExceptionGroup" => Self::ExceptionGroup,
|
||||
"staticmethod" => Self::Staticmethod,
|
||||
"classmethod" => Self::Classmethod,
|
||||
"Awaitable" => Self::Awaitable,
|
||||
"Generator" => Self::Generator,
|
||||
"deprecated" => Self::Deprecated,
|
||||
"GenericAlias" => Self::GenericAlias,
|
||||
"NoneType" => Self::NoneType,
|
||||
"ModuleType" => Self::ModuleType,
|
||||
"GeneratorType" => Self::GeneratorType,
|
||||
"AsyncGeneratorType" => Self::AsyncGeneratorType,
|
||||
"CoroutineType" => Self::CoroutineType,
|
||||
"FunctionType" => Self::FunctionType,
|
||||
"MethodType" => Self::MethodType,
|
||||
"UnionType" => Self::UnionType,
|
||||
|
@ -3627,11 +3659,14 @@ impl KnownClass {
|
|||
| Self::UnionType
|
||||
| Self::GeneratorType
|
||||
| Self::AsyncGeneratorType
|
||||
| Self::CoroutineType
|
||||
| Self::WrapperDescriptorType
|
||||
| Self::Field
|
||||
| Self::KwOnly
|
||||
| Self::InitVar
|
||||
| Self::NamedTupleFallback => module == self.canonical_module(db),
|
||||
| Self::NamedTupleFallback
|
||||
| Self::Awaitable
|
||||
| Self::Generator => module == self.canonical_module(db),
|
||||
Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types),
|
||||
Self::SpecialForm
|
||||
| Self::TypeVar
|
||||
|
|
|
@ -341,12 +341,16 @@ impl<'db> OverloadLiteral<'db> {
|
|||
GenericContext::from_type_params(db, index, type_params)
|
||||
});
|
||||
|
||||
let index = semantic_index(db, scope.file(db));
|
||||
let is_generator = scope.file_scope_id(db).is_generator_function(index);
|
||||
|
||||
Signature::from_function(
|
||||
db,
|
||||
generic_context,
|
||||
inherited_generic_context,
|
||||
definition,
|
||||
function_stmt_node,
|
||||
is_generator,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -3169,26 +3169,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
let context_expr = with_item.context_expr(self.module());
|
||||
let target = with_item.target(self.module());
|
||||
|
||||
let target_ty = if with_item.is_async() {
|
||||
let _context_expr_ty = self.infer_standalone_expression(context_expr);
|
||||
todo_type!("async `with` statement")
|
||||
} else {
|
||||
match with_item.target_kind() {
|
||||
TargetKind::Sequence(unpack_position, unpack) => {
|
||||
let unpacked = infer_unpack_types(self.db(), unpack);
|
||||
if unpack_position == UnpackPosition::First {
|
||||
self.context.extend(unpacked.diagnostics());
|
||||
}
|
||||
unpacked.expression_type(target)
|
||||
}
|
||||
TargetKind::Single => {
|
||||
let context_expr_ty = self.infer_standalone_expression(context_expr);
|
||||
self.infer_context_expression(
|
||||
context_expr,
|
||||
context_expr_ty,
|
||||
with_item.is_async(),
|
||||
)
|
||||
let target_ty = match with_item.target_kind() {
|
||||
TargetKind::Sequence(unpack_position, unpack) => {
|
||||
let unpacked = infer_unpack_types(self.db(), unpack);
|
||||
if unpack_position == UnpackPosition::First {
|
||||
self.context.extend(unpacked.diagnostics());
|
||||
}
|
||||
unpacked.expression_type(target)
|
||||
}
|
||||
TargetKind::Single => {
|
||||
let context_expr_ty = self.infer_standalone_expression(context_expr);
|
||||
self.infer_context_expression(context_expr, context_expr_ty, with_item.is_async())
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -3208,9 +3199,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
context_expression_type: Type<'db>,
|
||||
is_async: bool,
|
||||
) -> Type<'db> {
|
||||
// TODO: Handle async with statements (they use `aenter` and `aexit`)
|
||||
if is_async {
|
||||
return todo_type!("async `with` statement");
|
||||
return context_expression_type.aenter(self.db());
|
||||
}
|
||||
|
||||
context_expression_type
|
||||
|
@ -6102,8 +6092,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
err.fallback_element_type(self.db())
|
||||
});
|
||||
|
||||
// TODO get type from `ReturnType` of generator
|
||||
todo_type!("Generic `typing.Generator` type")
|
||||
iterable_type
|
||||
.generator_return_type(self.db())
|
||||
.unwrap_or_else(Type::unknown)
|
||||
}
|
||||
|
||||
fn infer_await_expression(&mut self, await_expression: &ast::ExprAwait) -> Type<'db> {
|
||||
|
@ -6112,8 +6103,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
node_index: _,
|
||||
value,
|
||||
} = await_expression;
|
||||
self.infer_expression(value);
|
||||
todo_type!("generic `typing.Awaitable` type")
|
||||
self.infer_expression(value).resolve_await(self.db())
|
||||
}
|
||||
|
||||
// Perform narrowing with applicable constraints between the current scope and the enclosing scope.
|
||||
|
|
|
@ -18,7 +18,7 @@ use smallvec::{SmallVec, smallvec_inline};
|
|||
use super::{DynamicType, Type, TypeTransformer, TypeVarVariance, definition_expression_type};
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::types::generics::{GenericContext, walk_generic_context};
|
||||
use crate::types::{TypeMapping, TypeRelation, TypeVarInstance, todo_type};
|
||||
use crate::types::{KnownClass, TypeMapping, TypeRelation, TypeVarInstance, todo_type};
|
||||
use crate::{Db, FxOrderSet};
|
||||
use ruff_python_ast::{self as ast, name::Name};
|
||||
|
||||
|
@ -320,14 +320,18 @@ impl<'db> Signature<'db> {
|
|||
inherited_generic_context: Option<GenericContext<'db>>,
|
||||
definition: Definition<'db>,
|
||||
function_node: &ast::StmtFunctionDef,
|
||||
is_generator: bool,
|
||||
) -> Self {
|
||||
let parameters =
|
||||
Parameters::from_parameters(db, definition, function_node.parameters.as_ref());
|
||||
let return_ty = function_node.returns.as_ref().map(|returns| {
|
||||
if function_node.is_async {
|
||||
todo_type!("generic types.CoroutineType")
|
||||
let plain_return_ty = definition_expression_type(db, definition, returns.as_ref());
|
||||
|
||||
if function_node.is_async && !is_generator {
|
||||
KnownClass::CoroutineType
|
||||
.to_specialized_instance(db, [Type::any(), Type::any(), plain_return_ty])
|
||||
} else {
|
||||
definition_expression_type(db, definition, returns.as_ref())
|
||||
plain_return_ty
|
||||
}
|
||||
});
|
||||
let legacy_generic_context =
|
||||
|
|
|
@ -75,14 +75,20 @@ impl<'db, 'ast> Unpacker<'db, 'ast> {
|
|||
);
|
||||
err.fallback_element_type(self.db())
|
||||
}),
|
||||
UnpackKind::ContextManager => value_type.try_enter(self.db()).unwrap_or_else(|err| {
|
||||
err.report_diagnostic(
|
||||
&self.context,
|
||||
value_type,
|
||||
value.as_any_node_ref(self.db(), self.module()),
|
||||
);
|
||||
err.fallback_enter_type(self.db())
|
||||
}),
|
||||
UnpackKind::ContextManager { is_async } => {
|
||||
if is_async {
|
||||
value_type.aenter(self.db())
|
||||
} else {
|
||||
value_type.try_enter(self.db()).unwrap_or_else(|err| {
|
||||
err.report_diagnostic(
|
||||
&self.context,
|
||||
value_type,
|
||||
value.as_any_node_ref(self.db(), self.module()),
|
||||
);
|
||||
err.fallback_enter_type(self.db())
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.unpack_inner(
|
||||
|
|
|
@ -104,7 +104,7 @@ pub(crate) enum UnpackKind {
|
|||
/// An iterable expression like the one in a `for` loop or a comprehension.
|
||||
Iterable,
|
||||
/// An context manager expression like the one in a `with` statement.
|
||||
ContextManager,
|
||||
ContextManager { is_async: bool },
|
||||
/// An expression that is being assigned to a target.
|
||||
Assign,
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue