Handle errors in __get__ calls

This commit is contained in:
David Peter
2025-02-20 09:15:35 +01:00
parent f406835639
commit ce3dcb066c
5 changed files with 260 additions and 24 deletions

View File

@@ -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")
```

View File

@@ -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

View File

@@ -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);

View File

@@ -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> {

View File

@@ -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>,