Handle errors in __get__ calls
This commit is contained in:
@@ -190,3 +190,57 @@ type IntOrStr = int | str
|
||||
|
||||
reveal_type(IntOrStr.__or__) # revealed: <bound method `__or__` of `typing.TypeAliasType`>
|
||||
```
|
||||
|
||||
## Error cases: Calling `__get__` for methods
|
||||
|
||||
The `__get__` method on `types.FunctionType` has the following overloaded signature in typeshed:
|
||||
|
||||
```py
|
||||
from types import FunctionType, MethodType
|
||||
from typing import overload
|
||||
|
||||
@overload
|
||||
def __get__(self, instance: None, owner: type, /) -> FunctionType: ...
|
||||
@overload
|
||||
def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ...
|
||||
```
|
||||
|
||||
Here, we test that this signature is enforced correctly:
|
||||
|
||||
```py
|
||||
from inspect import getattr_static
|
||||
|
||||
class C:
|
||||
def f(self, x: int) -> str:
|
||||
return "a"
|
||||
|
||||
method_wrapper = getattr_static(C, "f").__get__
|
||||
|
||||
reveal_type(method_wrapper) # revealed: <method-wrapper `__get__` of `f`>
|
||||
|
||||
# All of these are fine:
|
||||
method_wrapper(C(), C)
|
||||
method_wrapper(C())
|
||||
method_wrapper(C(), None)
|
||||
method_wrapper(None, C)
|
||||
|
||||
# Passing `None` without an `owner` argument is an
|
||||
# error: [missing-argument] "No argument provided for required parameter `owner`"
|
||||
method_wrapper(None)
|
||||
|
||||
# Passing something that is not assignable to `type` as the `owner` argument is an
|
||||
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`owner`); expected type `type`"
|
||||
method_wrapper(None, 1)
|
||||
|
||||
# Passing `None` as the `owner` argument when `instance` is `None` is an
|
||||
# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`owner`); expected type `type`"
|
||||
method_wrapper(None, None)
|
||||
|
||||
# Calling `__get__` without any arguments is an
|
||||
# error: [missing-argument] "No argument provided for required parameter `instance`"
|
||||
method_wrapper()
|
||||
|
||||
# Calling `__get__` with too many positional arguments is an
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments: expected 2, got 3"
|
||||
method_wrapper(C(), C, "one too many")
|
||||
```
|
||||
|
||||
@@ -343,22 +343,72 @@ Here, we only demonstrate how `__get__` works on functions:
|
||||
```py
|
||||
from inspect import getattr_static
|
||||
|
||||
def f(x: int) -> str:
|
||||
def f(x: object) -> str:
|
||||
return "a"
|
||||
|
||||
reveal_type(f) # revealed: Literal[f]
|
||||
reveal_type(f.__get__) # revealed: <method-wrapper `__get__` of `f`>
|
||||
reveal_type(f.__get__(None, f)) # revealed: Literal[f]
|
||||
reveal_type(f.__get__(None, f)(1)) # revealed: str
|
||||
reveal_type(f.__get__(None, type(f))) # revealed: Literal[f]
|
||||
reveal_type(f.__get__(None, type(f))(1)) # revealed: str
|
||||
|
||||
reveal_type(getattr_static(f, "__get__")) # revealed: <wrapper-descriptor `__get__` of `function` objects>
|
||||
reveal_type(getattr_static(f, "__get__")(f, None, type(f))) # revealed: Literal[f]
|
||||
wrapper_descriptor = getattr_static(f, "__get__")
|
||||
|
||||
reveal_type(wrapper_descriptor) # revealed: <wrapper-descriptor `__get__` of `function` objects>
|
||||
reveal_type(wrapper_descriptor(f, None, type(f))) # revealed: Literal[f]
|
||||
|
||||
# Attribute access on the method-wrapper `f.__get__` falls back to `MethodWrapperType`:
|
||||
reveal_type(f.__get__.__hash__) # revealed: <bound method `__hash__` of `MethodWrapperType`>
|
||||
|
||||
# Attribute access on the wrapper-descriptor `getattr_static(f, "__get__")` falls back to `WrapperDescriptorType`:
|
||||
reveal_type(getattr_static(f, "__get__").__qualname__) # revealed: @Todo(@property)
|
||||
# Attribute access on the wrapper-descriptor falls back to `WrapperDescriptorType`:
|
||||
reveal_type(wrapper_descriptor.__qualname__) # revealed: @Todo(@property)
|
||||
```
|
||||
|
||||
We can also bind the free function `f` to an instance of a class `C`:
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
|
||||
bound_method = wrapper_descriptor(f, C(), C)
|
||||
|
||||
reveal_type(bound_method) # revealed: <bound method `f` of `C`>
|
||||
```
|
||||
|
||||
We can then call it, and the instance of `C` is implicitly passed to the first parameter of `f`
|
||||
(`x`):
|
||||
|
||||
```py
|
||||
reveal_type(bound_method()) # revealed: str
|
||||
```
|
||||
|
||||
Finally, we test some error cases for the call to the wrapper descriptor:
|
||||
|
||||
```py
|
||||
# Calling the wrapper descriptor without any arguments is an
|
||||
# error: [missing-argument] "No arguments provided for required parameters `self`, `instance`"
|
||||
wrapper_descriptor()
|
||||
|
||||
# Calling it without the `instance` argument is an also an
|
||||
# error: [missing-argument] "No argument provided for required parameter `instance`"
|
||||
wrapper_descriptor(f)
|
||||
|
||||
# Calling it without the `owner` argument if `instance` is not `None` is an
|
||||
# error: [missing-argument] "No argument provided for required parameter `owner`"
|
||||
wrapper_descriptor(f, None)
|
||||
|
||||
# But calling it with an instance is fine (in this case, the `owner` argument is optional):
|
||||
wrapper_descriptor(f, C())
|
||||
|
||||
# Calling it with something that is not a `FunctionType` as the first argument is an
|
||||
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`self`); expected type `FunctionType`"
|
||||
wrapper_descriptor(1, None, type(f))
|
||||
|
||||
# Calling it with something that is not a `type` as the `owner` argument is an
|
||||
# error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 3 (`owner`); expected type `type`"
|
||||
wrapper_descriptor(f, None, f)
|
||||
|
||||
# Calling it with too many positional arguments is an
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments: expected 3, got 4"
|
||||
wrapper_descriptor(f, None, type(f), "one too many")
|
||||
```
|
||||
|
||||
[descriptors]: https://docs.python.org/3/howto/descriptor.html
|
||||
|
||||
@@ -42,6 +42,7 @@ use crate::types::diagnostic::INVALID_TYPE_FORM;
|
||||
use crate::types::infer::infer_unpack_types;
|
||||
use crate::types::mro::{Mro, MroError, MroIterator};
|
||||
pub(crate) use crate::types::narrow::narrowing_constraint;
|
||||
use crate::types::signatures::{Parameter, ParameterKind, Parameters};
|
||||
use crate::{Db, FxOrderSet, Module, Program};
|
||||
|
||||
mod builder;
|
||||
@@ -1791,25 +1792,128 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
Type::Callable(CallableType::MethodWrapperDunderGet(function)) => {
|
||||
let return_ty = match arguments.first_argument() {
|
||||
Some(ty) if ty.is_none(db) => Type::FunctionLiteral(function),
|
||||
Some(instance) => Type::Callable(CallableType::BoundMethod(
|
||||
BoundMethodType::new(db, function, instance),
|
||||
)),
|
||||
_ => Type::unknown(),
|
||||
};
|
||||
Ok(CallOutcome::Single(CallBinding::from_return_type(
|
||||
return_ty,
|
||||
)))
|
||||
// Here, we dynamically model the overloaded function signature of `types.FunctionType.__get__`.
|
||||
// This is required because we need to return more precise types than what the signature in
|
||||
// typeshed provides:
|
||||
//
|
||||
// ```py
|
||||
// class FunctionType:
|
||||
// # ...
|
||||
// @overload
|
||||
// def __get__(self, instance: None, owner: type, /) -> FunctionType: ...
|
||||
// @overload
|
||||
// def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ...
|
||||
// ```
|
||||
|
||||
let first_argument_is_none =
|
||||
arguments.first_argument().is_some_and(|ty| ty.is_none(db));
|
||||
|
||||
let signature = Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::new(
|
||||
Some("instance".into()),
|
||||
Some(Type::object(db)),
|
||||
ParameterKind::PositionalOnly { default_ty: None },
|
||||
),
|
||||
if first_argument_is_none {
|
||||
Parameter::new(
|
||||
Some("owner".into()),
|
||||
Some(KnownClass::Type.to_instance(db)),
|
||||
ParameterKind::PositionalOnly { default_ty: None },
|
||||
)
|
||||
} else {
|
||||
Parameter::new(
|
||||
Some("owner".into()),
|
||||
Some(UnionType::from_elements(
|
||||
db,
|
||||
[KnownClass::Type.to_instance(db), Type::none(db)],
|
||||
)),
|
||||
ParameterKind::PositionalOnly {
|
||||
default_ty: Some(Type::none(db)),
|
||||
},
|
||||
)
|
||||
},
|
||||
]),
|
||||
Some(match arguments.first_argument() {
|
||||
Some(ty) if ty.is_none(db) => Type::FunctionLiteral(function),
|
||||
Some(instance) => Type::Callable(CallableType::BoundMethod(
|
||||
BoundMethodType::new(db, function, instance),
|
||||
)),
|
||||
_ => Type::unknown(),
|
||||
}),
|
||||
);
|
||||
|
||||
let binding = bind_call(db, arguments, &signature, self);
|
||||
|
||||
if binding.has_binding_errors() {
|
||||
Err(CallError::BindingError { binding })
|
||||
} else {
|
||||
Ok(CallOutcome::Single(binding))
|
||||
}
|
||||
}
|
||||
Type::Callable(CallableType::WrapperDescriptorDunderGet) => {
|
||||
let return_ty = match arguments.first_argument() {
|
||||
Some(f @ Type::FunctionLiteral(_)) => f,
|
||||
_ => Type::unknown(),
|
||||
};
|
||||
Ok(CallOutcome::Single(CallBinding::from_return_type(
|
||||
return_ty,
|
||||
)))
|
||||
// Here, we also model `types.FunctionType.__get__`, but now we consider a call to
|
||||
// this as a function, i.e. we also expect the `self` argument to be passed in.
|
||||
|
||||
let second_argument_is_none = arguments
|
||||
.second_argument()
|
||||
.map_or(false, |ty| ty.is_none(db));
|
||||
|
||||
let signature = Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::new(
|
||||
Some("self".into()),
|
||||
Some(KnownClass::FunctionType.to_instance(db)),
|
||||
ParameterKind::PositionalOnly { default_ty: None },
|
||||
),
|
||||
Parameter::new(
|
||||
Some("instance".into()),
|
||||
Some(Type::object(db)),
|
||||
ParameterKind::PositionalOnly { default_ty: None },
|
||||
),
|
||||
if second_argument_is_none {
|
||||
Parameter::new(
|
||||
Some("owner".into()),
|
||||
Some(KnownClass::Type.to_instance(db)),
|
||||
ParameterKind::PositionalOnly { default_ty: None },
|
||||
)
|
||||
} else {
|
||||
Parameter::new(
|
||||
Some("owner".into()),
|
||||
Some(UnionType::from_elements(
|
||||
db,
|
||||
[KnownClass::Type.to_instance(db), Type::none(db)],
|
||||
)),
|
||||
ParameterKind::PositionalOnly {
|
||||
default_ty: Some(Type::none(db)),
|
||||
},
|
||||
)
|
||||
},
|
||||
]),
|
||||
Some(
|
||||
match (arguments.first_argument(), arguments.second_argument()) {
|
||||
(Some(function @ Type::FunctionLiteral(_)), Some(instance))
|
||||
if instance.is_none(db) =>
|
||||
{
|
||||
function
|
||||
}
|
||||
(Some(Type::FunctionLiteral(function)), Some(instance)) => {
|
||||
Type::Callable(CallableType::BoundMethod(BoundMethodType::new(
|
||||
db, function, instance,
|
||||
)))
|
||||
}
|
||||
_ => Type::unknown(),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
let binding = bind_call(db, arguments, &signature, self);
|
||||
|
||||
if binding.has_binding_errors() {
|
||||
Err(CallError::BindingError { binding })
|
||||
} else {
|
||||
Ok(CallOutcome::Single(binding))
|
||||
}
|
||||
}
|
||||
Type::FunctionLiteral(function_type) => {
|
||||
let mut binding = bind_call(db, arguments, function_type.signature(db), self);
|
||||
|
||||
@@ -29,6 +29,11 @@ impl<'a, 'db> CallArguments<'a, 'db> {
|
||||
pub(crate) fn first_argument(&self) -> Option<Type<'db>> {
|
||||
self.0.first().map(Argument::ty)
|
||||
}
|
||||
|
||||
// TODO this should be eliminated in favor of [`bind_call`]
|
||||
pub(crate) fn second_argument(&self) -> Option<Type<'db>> {
|
||||
self.0.get(1).map(Argument::ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db, 'a, 'b> IntoIterator for &'b CallArguments<'a, 'db> {
|
||||
|
||||
@@ -21,6 +21,13 @@ pub(crate) struct Signature<'db> {
|
||||
}
|
||||
|
||||
impl<'db> Signature<'db> {
|
||||
pub(crate) fn new(parameters: Parameters<'db>, return_ty: Option<Type<'db>>) -> Self {
|
||||
Self {
|
||||
parameters,
|
||||
return_ty,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a todo signature: (*args: Todo, **kwargs: Todo) -> Todo
|
||||
pub(crate) fn todo() -> Self {
|
||||
Self {
|
||||
@@ -64,6 +71,10 @@ impl<'db> Signature<'db> {
|
||||
pub(crate) struct Parameters<'db>(Vec<Parameter<'db>>);
|
||||
|
||||
impl<'db> Parameters<'db> {
|
||||
pub(crate) fn new(parameters: impl IntoIterator<Item = Parameter<'db>>) -> Self {
|
||||
Self(parameters.into_iter().collect())
|
||||
}
|
||||
|
||||
/// Return todo parameters: (*args: Todo, **kwargs: Todo)
|
||||
fn todo() -> Self {
|
||||
Self(vec![
|
||||
@@ -233,6 +244,18 @@ pub(crate) struct Parameter<'db> {
|
||||
}
|
||||
|
||||
impl<'db> Parameter<'db> {
|
||||
pub(crate) fn new(
|
||||
name: Option<Name>,
|
||||
annotated_ty: Option<Type<'db>>,
|
||||
kind: ParameterKind<'db>,
|
||||
) -> Self {
|
||||
Self {
|
||||
name,
|
||||
annotated_ty,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_node_and_kind(
|
||||
db: &'db dyn Db,
|
||||
definition: Definition<'db>,
|
||||
|
||||
Reference in New Issue
Block a user