diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md index 6ebe172564..6aec3e68a9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md @@ -190,3 +190,57 @@ type IntOrStr = int | str reveal_type(IntOrStr.__or__) # revealed: ``` + +## 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: + +# 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") +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md index 1c422eaf4b..42ada703dc 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md @@ -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: -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: -reveal_type(getattr_static(f, "__get__")(f, None, type(f))) # revealed: Literal[f] +wrapper_descriptor = getattr_static(f, "__get__") + +reveal_type(wrapper_descriptor) # revealed: +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: -# 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: +``` + +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 diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index fe4a9cb330..c70ccea6a6 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -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); diff --git a/crates/red_knot_python_semantic/src/types/call/arguments.rs b/crates/red_knot_python_semantic/src/types/call/arguments.rs index 8cc2027c87..dafd2b4d53 100644 --- a/crates/red_knot_python_semantic/src/types/call/arguments.rs +++ b/crates/red_knot_python_semantic/src/types/call/arguments.rs @@ -29,6 +29,11 @@ impl<'a, 'db> CallArguments<'a, 'db> { pub(crate) fn first_argument(&self) -> Option> { self.0.first().map(Argument::ty) } + + // TODO this should be eliminated in favor of [`bind_call`] + pub(crate) fn second_argument(&self) -> Option> { + self.0.get(1).map(Argument::ty) + } } impl<'db, 'a, 'b> IntoIterator for &'b CallArguments<'a, 'db> { diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 3bc41ced2c..8a75211864 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -21,6 +21,13 @@ pub(crate) struct Signature<'db> { } impl<'db> Signature<'db> { + pub(crate) fn new(parameters: Parameters<'db>, return_ty: Option>) -> 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>); impl<'db> Parameters<'db> { + pub(crate) fn new(parameters: impl IntoIterator>) -> 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, + annotated_ty: Option>, + kind: ParameterKind<'db>, + ) -> Self { + Self { + name, + annotated_ty, + kind, + } + } + fn from_node_and_kind( db: &'db dyn Db, definition: Definition<'db>,