diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md index db94217aa2..cf586bacc7 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/literal_string.md @@ -73,12 +73,12 @@ qux = (foo, bar) reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]] # TODO: Infer "LiteralString" -reveal_type(foo.join(qux)) # revealed: @Todo(Attribute access on `StringLiteral` types) +reveal_type(foo.join(qux)) # revealed: @Todo(decorated method) template: LiteralString = "{}, {}" reveal_type(template) # revealed: Literal["{}, {}"] # TODO: Infer `LiteralString` -reveal_type(template.format(foo, bar)) # revealed: @Todo(Attribute access on `StringLiteral` types) +reveal_type(template.format(foo, bar)) # revealed: @Todo(decorated method) ``` ### Assignability diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md index eee6f9f269..6e169fe22b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -1005,8 +1005,8 @@ reveal_type(f.__kwdefaults__) # revealed: @Todo(generics) | None Some attributes are special-cased, however: ```py -reveal_type(f.__get__) # revealed: @Todo(`__get__` method on functions) -reveal_type(f.__call__) # revealed: @Todo(`__call__` method on functions) +reveal_type(f.__get__) # revealed: +reveal_type(f.__call__) # revealed: ``` ### Int-literal attributes @@ -1015,7 +1015,7 @@ Most attribute accesses on int-literal types are delegated to `builtins.int`, si integers are instances of that class: ```py -reveal_type((2).bit_length) # revealed: @Todo(bound method) +reveal_type((2).bit_length) # revealed: reveal_type((2).denominator) # revealed: @Todo(@property) ``` @@ -1029,11 +1029,11 @@ reveal_type((2).real) # revealed: Literal[2] ### Bool-literal attributes Most attribute accesses on bool-literal types are delegated to `builtins.bool`, since all literal -bols are instances of that class: +bools are instances of that class: ```py -reveal_type(True.__and__) # revealed: @Todo(bound method) -reveal_type(False.__or__) # revealed: @Todo(bound method) +reveal_type(True.__and__) # revealed: @Todo(decorated method) +reveal_type(False.__or__) # revealed: @Todo(decorated method) ``` Some attributes are special-cased, however: @@ -1045,11 +1045,11 @@ reveal_type(False.real) # revealed: Literal[0] ### Bytes-literal attributes -All attribute access on literal `bytes` types is currently delegated to `buitins.bytes`: +All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`: ```py -reveal_type(b"foo".join) # revealed: @Todo(bound method) -reveal_type(b"foo".endswith) # revealed: @Todo(bound method) +reveal_type(b"foo".join) # revealed: +reveal_type(b"foo".endswith) # revealed: ``` ## Instance attribute edge cases @@ -1136,6 +1136,40 @@ class C: reveal_type(C().x) # revealed: Unknown ``` +### Builtin types attributes + +This test can probably be removed eventually, but we currently include it because we do not yet +understand generic bases and protocols, and we want to make sure that we can still use builtin types +in our tests in the meantime. See the corresponding TODO in `Type::static_member` for more +information. + +```py +class C: + a_int: int = 1 + a_str: str = "a" + a_bytes: bytes = b"a" + a_bool: bool = True + a_float: float = 1.0 + a_complex: complex = 1 + 1j + a_tuple: tuple[int] = (1,) + a_range: range = range(1) + a_slice: slice = slice(1) + a_type: type = int + a_none: None = None + +reveal_type(C.a_int) # revealed: int +reveal_type(C.a_str) # revealed: str +reveal_type(C.a_bytes) # revealed: bytes +reveal_type(C.a_bool) # revealed: bool +reveal_type(C.a_float) # revealed: int | float +reveal_type(C.a_complex) # revealed: int | float | complex +reveal_type(C.a_tuple) # revealed: tuple[int] +reveal_type(C.a_range) # revealed: range +reveal_type(C.a_slice) # revealed: slice +reveal_type(C.a_type) # revealed: type +reveal_type(C.a_none) # revealed: None +``` + ## References Some of the tests in the *Class and instance variables* section draw inspiration from diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/getattr_static.md b/crates/red_knot_python_semantic/resources/mdtest/call/getattr_static.md new file mode 100644 index 0000000000..01d252ff96 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/call/getattr_static.md @@ -0,0 +1,133 @@ +# `inspect.getattr_static` + +## Basic usage + +`inspect.getattr_static` is a function that returns attributes of an object without invoking the +descriptor protocol (for caveats, see the [official documentation]). + +Consider the following example: + +```py +import inspect + +class Descriptor: + def __get__(self, instance, owner) -> str: + return 1 + +class C: + normal: int = 1 + descriptor: Descriptor = Descriptor() +``` + +If we access attributes on an instance of `C` as usual, the descriptor protocol is invoked, and we +get a type of `str` for the `descriptor` attribute: + +```py +c = C() + +reveal_type(c.normal) # revealed: int +reveal_type(c.descriptor) # revealed: str +``` + +However, if we use `inspect.getattr_static`, we can see the underlying `Descriptor` type: + +```py +reveal_type(inspect.getattr_static(c, "normal")) # revealed: int +reveal_type(inspect.getattr_static(c, "descriptor")) # revealed: Descriptor +``` + +For non-existent attributes, a default value can be provided: + +```py +reveal_type(inspect.getattr_static(C, "normal", "default-arg")) # revealed: int +reveal_type(inspect.getattr_static(C, "non_existent", "default-arg")) # revealed: Literal["default-arg"] +``` + +When a non-existent attribute is accessed without a default value, the runtime raises an +`AttributeError`. We could emit a diagnostic for this case, but that is currently not supported: + +```py +# TODO: we could emit a diagnostic here +reveal_type(inspect.getattr_static(C, "non_existent")) # revealed: Never +``` + +We can access attributes on objects of all kinds: + +```py +import sys + +reveal_type(inspect.getattr_static(sys, "platform")) # revealed: LiteralString +reveal_type(inspect.getattr_static(inspect, "getattr_static")) # revealed: Literal[getattr_static] + +reveal_type(inspect.getattr_static(1, "real")) # revealed: Literal[1] +``` + +(Implicit) instance attributes can also be accessed through `inspect.getattr_static`: + +```py +class D: + def __init__(self) -> None: + self.instance_attr: int = 1 + +reveal_type(inspect.getattr_static(D(), "instance_attr")) # revealed: int +``` + +## Error cases + +We can only infer precise types if the attribute is a literal string. In all other cases, we fall +back to `Any`: + +```py +import inspect + +class C: + x: int = 1 + +def _(attr_name: str): + reveal_type(inspect.getattr_static(C(), attr_name)) # revealed: Any + reveal_type(inspect.getattr_static(C(), attr_name, 1)) # revealed: Any +``` + +But we still detect errors in the number or type of arguments: + +```py +# error: [missing-argument] "No arguments provided for required parameters `obj`, `attr` of function `getattr_static`" +inspect.getattr_static() + +# error: [missing-argument] "No argument provided for required parameter `attr`" +inspect.getattr_static(C()) + +# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`attr`) of function `getattr_static`; expected type `str`" +inspect.getattr_static(C(), 1) + +# error: [too-many-positional-arguments] "Too many positional arguments to function `getattr_static`: expected 3, got 4" +inspect.getattr_static(C(), "x", "default-arg", "one too many") +``` + +## Possibly unbound attributes + +```py +import inspect + +def _(flag: bool): + class C: + if flag: + x: int = 1 + + reveal_type(inspect.getattr_static(C, "x", "default")) # revealed: int | Literal["default"] +``` + +## Gradual types + +```py +import inspect +from typing import Any + +def _(a: Any, tuple_of_any: tuple[Any]): + reveal_type(inspect.getattr_static(a, "x", "default")) # revealed: Any | Literal["default"] + + # TODO: Ideally, this would just be `Literal[index]` + reveal_type(inspect.getattr_static(tuple_of_any, "index", "default")) # revealed: Literal[index] | Literal["default"] +``` + +[official documentation]: https://docs.python.org/3/library/inspect.html#inspect.getattr_static diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md new file mode 100644 index 0000000000..7e9a17bda2 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md @@ -0,0 +1,258 @@ +# Methods + +## Background: Functions as descriptors + +> Note: See also this related section in the descriptor guide: [Functions and methods]. + +Say we have a simple class `C` with a function definition `f` inside its body: + +```py +class C: + def f(self, x: int) -> str: + return "a" +``` + +Whenever we access the `f` attribute through the class object itself (`C.f`) or through an instance +(`C().f`), this access happens via the descriptor protocol. Functions are (non-data) descriptors +because they implement a `__get__` method. This is crucial in making sure that method calls work as +expected. In general, the signature of the `__get__` method in the descriptor protocol is +`__get__(self, instance, owner)`. The `self` argument is the descriptor object itself (`f`). The +passed value for the `instance` argument depends on whether the attribute is accessed from the class +object (in which case it is `None`), or from an instance (in which case it is the instance of type +`C`). The `owner` argument is the class itself (`C` of type `Literal[C]`). To summarize: + +- `C.f` is equivalent to `getattr_static(C, "f").__get__(None, C)` +- `C().f` is equivalent to `getattr_static(C, "f").__get__(C(), C)` + +Here, `inspect.getattr_static` is used to bypass the descriptor protocol and directly access the +function attribute. The way the special `__get__` method *on functions* works is as follows. In the +former case, if the `instance` argument is `None`, `__get__` simply returns the function itself. In +the latter case, it returns a *bound method* object: + +```py +from inspect import getattr_static + +reveal_type(getattr_static(C, "f")) # revealed: Literal[f] + +reveal_type(getattr_static(C, "f").__get__) # revealed: + +reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: Literal[f] +reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: +``` + +In conclusion, this is why we see the following two types when accessing the `f` attribute on the +class object `C` and on an instance `C()`: + +```py +reveal_type(C.f) # revealed: Literal[f] +reveal_type(C().f) # revealed: +``` + +A bound method is a callable object that contains a reference to the `instance` that it was called +on (can be inspected via `__self__`), and the function object that it refers to (can be inspected +via `__func__`): + +```py +bound_method = C().f + +reveal_type(bound_method.__self__) # revealed: C +reveal_type(bound_method.__func__) # revealed: Literal[f] +``` + +When we call the bound method, the `instance` is implicitly passed as the first argument (`self`): + +```py +reveal_type(C().f(1)) # revealed: str +reveal_type(bound_method(1)) # revealed: str +``` + +When we call the function object itself, we need to pass the `instance` explicitly: + +```py +C.f(1) # error: [missing-argument] + +reveal_type(C.f(C(), 1)) # revealed: str +``` + +When we access methods from derived classes, they will be bound to instances of the derived class: + +```py +class D(C): + pass + +reveal_type(D().f) # revealed: +``` + +If we access an attribute on a bound method object itself, it will defer to `types.MethodType`: + +```py +reveal_type(bound_method.__hash__) # revealed: +``` + +If an attribute is not available on the bound method object, it will be looked up on the underlying +function object. We model this explicitly, which means that we can access `__kwdefaults__` on bound +methods, even though it is not available on `types.MethodType`: + +```py +reveal_type(bound_method.__kwdefaults__) # revealed: @Todo(generics) | None +``` + +## Basic method calls on class objects and instances + +```py +class Base: + def method_on_base(self, x: int | None) -> str: + return "a" + +class Derived(Base): + def method_on_derived(self, x: bytes) -> tuple[int, str]: + return (1, "a") + +reveal_type(Base().method_on_base(1)) # revealed: str +reveal_type(Base.method_on_base(Base(), 1)) # revealed: str + +Base().method_on_base("incorrect") # error: [invalid-argument-type] +Base().method_on_base() # error: [missing-argument] +Base().method_on_base(1, 2) # error: [too-many-positional-arguments] + +reveal_type(Derived().method_on_base(1)) # revealed: str +reveal_type(Derived().method_on_derived(b"abc")) # revealed: tuple[int, str] +reveal_type(Derived.method_on_base(Derived(), 1)) # revealed: str +reveal_type(Derived.method_on_derived(Derived(), b"abc")) # revealed: tuple[int, str] +``` + +## Method calls on literals + +### Boolean literals + +```py +reveal_type(True.bit_length()) # revealed: int +reveal_type(True.as_integer_ratio()) # revealed: tuple[int, Literal[1]] +``` + +### Integer literals + +```py +reveal_type((42).bit_length()) # revealed: int +``` + +### String literals + +```py +reveal_type("abcde".find("abc")) # revealed: int +reveal_type("foo".encode(encoding="utf-8")) # revealed: bytes + +"abcde".find(123) # error: [invalid-argument-type] +``` + +### Bytes literals + +```py +reveal_type(b"abcde".startswith(b"abc")) # revealed: bool +``` + +## Method calls on `LiteralString` + +```py +from typing_extensions import LiteralString + +def f(s: LiteralString) -> None: + reveal_type(s.find("a")) # revealed: int +``` + +## Method calls on `tuple` + +```py +def f(t: tuple[int, str]) -> None: + reveal_type(t.index("a")) # revealed: int +``` + +## Method calls on unions + +```py +from typing import Any + +class A: + def f(self) -> int: + return 1 + +class B: + def f(self) -> str: + return "a" + +def f(a_or_b: A | B, any_or_a: Any | A): + reveal_type(a_or_b.f) # revealed: | + reveal_type(a_or_b.f()) # revealed: int | str + + reveal_type(any_or_a.f) # revealed: Any | + reveal_type(any_or_a.f()) # revealed: Any | int +``` + +## Method calls on `KnownInstance` types + +```toml +[environment] +python-version = "3.12" +``` + +```py +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") +``` + +[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods 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 13c79c4aad..74dd203b02 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md @@ -22,22 +22,26 @@ class Ten: pass class C: - ten = Ten() + ten: Ten = Ten() c = C() -# TODO: this should be `Literal[10]` -reveal_type(c.ten) # revealed: Unknown | Ten +reveal_type(c.ten) # revealed: Literal[10] -# TODO: This should `Literal[10]` -reveal_type(C.ten) # revealed: Unknown | Ten +reveal_type(C.ten) # revealed: Literal[10] # These are fine: -c.ten = 10 +# TODO: This should not be an error +c.ten = 10 # error: [invalid-assignment] C.ten = 10 -# TODO: Both of these should be errors +# TODO: This should be an error (as the wrong type is being implicitly passed to `Ten.__set__`), +# but the error message is misleading. +# error: [invalid-assignment] "Object of type `Literal[11]` is not assignable to attribute `ten` of type `Ten`" c.ten = 11 + +# TODO: same as above +# error: [invalid-assignment] "Object of type `Literal[11]` is not assignable to attribute `ten` of type `Literal[10]`" C.ten = 11 ``` @@ -57,24 +61,86 @@ class FlexibleInt: self._value = int(value) class C: - flexible_int = FlexibleInt() + flexible_int: FlexibleInt = FlexibleInt() c = C() -# TODO: should be `int | None` -reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt +reveal_type(c.flexible_int) # revealed: int | None +# TODO: These should not be errors +# error: [invalid-assignment] c.flexible_int = 42 # okay +# error: [invalid-assignment] c.flexible_int = "42" # also okay! -# TODO: should be `int | None` -reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt +reveal_type(c.flexible_int) # revealed: int | None -# TODO: should be an error +# TODO: This should be an error, but the message needs to be improved. +# error: [invalid-assignment] "Object of type `None` is not assignable to attribute `flexible_int` of type `FlexibleInt`" c.flexible_int = None # not okay -# TODO: should be `int | None` -reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt +reveal_type(c.flexible_int) # revealed: int | None +``` + +## Data and non-data descriptors + +Descriptors that define `__set__` or `__delete__` are called *data descriptors*. An example\ +of a data descriptor is a `property` with a setter and/or a deleter.\ +Descriptors that only define `__get__`, meanwhile, are called *non-data descriptors*. Examples +include\ +functions, `classmethod` or `staticmethod`). + +The precedence chain for attribute access is (1) data descriptors, (2) instance attributes, and (3) +non-data descriptors. + +```py +from typing import Literal + +class DataDescriptor: + def __get__(self, instance: object, owner: type | None = None) -> Literal["data"]: + return "data" + + def __set__(self, instance: int, value) -> None: + pass + +class NonDataDescriptor: + def __get__(self, instance: object, owner: type | None = None) -> Literal["non-data"]: + return "non-data" + +class C: + data_descriptor = DataDescriptor() + non_data_descriptor = NonDataDescriptor() + + def f(self): + # This explains why data descriptors come first in the precedence chain. If + # instance attributes would take priority, we would override the descriptor + # here. Instead, this calls `DataDescriptor.__set__`, i.e. it does not affect + # the type of the `data_descriptor` attribute. + self.data_descriptor = 1 + + # However, for non-data descriptors, instance attributes do take precedence. + # So it is possible to override them. + self.non_data_descriptor = 1 + +c = C() + +# TODO: This should ideally be `Unknown | Literal["data"]`. +# +# - Pyright also wrongly shows `int | Literal['data']` here +# - Mypy shows Literal["data"] here, but also shows Literal["non-data"] below. +# +reveal_type(c.data_descriptor) # revealed: Unknown | Literal["data", 1] + +reveal_type(c.non_data_descriptor) # revealed: Unknown | Literal["non-data", 1] + +reveal_type(C.data_descriptor) # revealed: Unknown | Literal["data"] + +reveal_type(C.non_data_descriptor) # revealed: Unknown | Literal["non-data"] + +# It is possible to override data descriptors via class objects. The following +# assignment does not call `DataDescriptor.__set__`. For this reason, we infer +# `Unknown | …` for all (descriptor) attributes. +C.data_descriptor = "something else" # This is okay ``` ## Built-in `property` descriptor @@ -101,7 +167,7 @@ c = C() reveal_type(c._name) # revealed: str | None # Should be `str` -reveal_type(c.name) # revealed: @Todo(bound method) +reveal_type(c.name) # revealed: @Todo(decorated method) # Should be `builtins.property` reveal_type(C.name) # revealed: Literal[name] @@ -142,7 +208,7 @@ reveal_type(c1) # revealed: @Todo(return type) reveal_type(C.get_name()) # revealed: @Todo(return type) # TODO: should be `str` -reveal_type(C("42").get_name()) # revealed: @Todo(bound method) +reveal_type(C("42").get_name()) # revealed: @Todo(decorated method) ``` ## Descriptors only work when used as class variables @@ -160,9 +226,10 @@ class Ten: class C: def __init__(self): - self.ten = Ten() + self.ten: Ten = Ten() -reveal_type(C().ten) # revealed: Unknown | Ten +# TODO: Should be Ten +reveal_type(C().ten) # revealed: Literal[10] ``` ## Descriptors distinguishing between class and instance access @@ -186,13 +253,166 @@ class Descriptor: return "called on class object" class C: - d = Descriptor() + d: Descriptor = Descriptor() # TODO: should be `Literal["called on class object"] -reveal_type(C.d) # revealed: Unknown | Descriptor +reveal_type(C.d) # revealed: LiteralString # TODO: should be `Literal["called on instance"] -reveal_type(C().d) # revealed: Unknown | Descriptor +reveal_type(C().d) # revealed: LiteralString +``` + +## Undeclared descriptor arguments + +If a descriptor attribute is not declared, we union with `Unknown`, just like for regular +attributes, since that attribute could be overwritten externally. Even a data descriptor with a +`__set__` method can be overwritten when accessed through a class object. + +```py +class Descriptor: + def __get__(self, instance: object, owner: type | None = None) -> int: + return 1 + + def __set__(self, instance: object, value: int) -> None: + pass + +class C: + descriptor = Descriptor() + +C.descriptor = "something else" + +# This could also be `Literal["something else"]` if we support narrowing of attribute types based on assignments +reveal_type(C.descriptor) # revealed: Unknown | int +``` + +## Descriptors with incorrect `__get__` signature + +```py +class Descriptor: + # `__get__` method with missing parameters: + def __get__(self) -> int: + return 1 + +class C: + descriptor: Descriptor = Descriptor() + +# TODO: This should be an error +reveal_type(C.descriptor) # revealed: Descriptor +``` + +## Possibly-unbound `__get__` method + +```py +def _(flag: bool): + class MaybeDescriptor: + if flag: + def __get__(self, instance: object, owner: type | None = None) -> int: + return 1 + + class C: + descriptor: MaybeDescriptor = MaybeDescriptor() + + # TODO: This should be `MaybeDescriptor | int` + reveal_type(C.descriptor) # revealed: int +``` + +## Dunder methods + +Dunder methods are looked up on the meta type, but we still need to invoke the descriptor protocol: + +```py +class SomeCallable: + def __call__(self, x: int) -> str: + return "a" + +class Descriptor: + def __get__(self, instance: object, owner: type | None = None) -> SomeCallable: + return SomeCallable() + +class B: + __call__: Descriptor = Descriptor() + +b_instance = B() +reveal_type(b_instance(1)) # revealed: str + +b_instance("bla") # error: [invalid-argument-type] +``` + +## Functions as descriptors + +Functions are descriptors because they implement a `__get__` method. This is crucial in making sure +that method calls work as expected. See [this test suite](./call/methods.md) for more information. +Here, we only demonstrate how `__get__` works on functions: + +```py +from inspect import getattr_static + +def f(x: object) -> str: + return "a" + +reveal_type(f) # revealed: Literal[f] +reveal_type(f.__get__) # revealed: +reveal_type(f.__get__(None, type(f))) # revealed: Literal[f] +reveal_type(f.__get__(None, type(f))(1)) # revealed: str + +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 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/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md new file mode 100644 index 0000000000..fe14d0024f --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -0,0 +1,15 @@ +# Protocols + +We do not support protocols yet, but to avoid false positives, we *partially* support some known +protocols. + +## `typing.SupportsIndex` + +```py +from typing import SupportsIndex, Literal + +def _(some_int: int, some_literal_int: Literal[1], some_indexable: SupportsIndex): + a: SupportsIndex = some_int + b: SupportsIndex = some_literal_int + c: SupportsIndex = some_indexable +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md index b2082bc1dd..9c585c9f5f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md +++ b/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md @@ -9,7 +9,7 @@ is unbound. ```py reveal_type(__name__) # revealed: str reveal_type(__file__) # revealed: str | None -reveal_type(__loader__) # revealed: LoaderProtocol | None +reveal_type(__loader__) # revealed: @Todo(instance attribute on class with dynamic base) | None reveal_type(__package__) # revealed: str | None reveal_type(__doc__) # revealed: str | None @@ -54,10 +54,10 @@ inside the module: import typing reveal_type(typing.__name__) # revealed: str -reveal_type(typing.__init__) # revealed: @Todo(bound method) +reveal_type(typing.__init__) # revealed: # These come from `builtins.object`, not `types.ModuleType`: -reveal_type(typing.__eq__) # revealed: @Todo(bound method) +reveal_type(typing.__eq__) # revealed: reveal_type(typing.__class__) # revealed: Literal[ModuleType] diff --git a/crates/red_knot_python_semantic/resources/mdtest/sys_platform.md b/crates/red_knot_python_semantic/resources/mdtest/sys_platform.md index 7b9c0cdf9c..6c980b65c6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/sys_platform.md +++ b/crates/red_knot_python_semantic/resources/mdtest/sys_platform.md @@ -66,6 +66,6 @@ It is [recommended](https://docs.python.org/3/library/sys.html#sys.platform) to ```py import sys -reveal_type(sys.platform.startswith("freebsd")) # revealed: @Todo(Attribute access on `LiteralString` types) -reveal_type(sys.platform.startswith("linux")) # revealed: @Todo(Attribute access on `LiteralString` types) +reveal_type(sys.platform.startswith("freebsd")) # revealed: bool +reveal_type(sys.platform.startswith("linux")) # revealed: bool ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_of/dynamic.md b/crates/red_knot_python_semantic/resources/mdtest/type_of/dynamic.md index 5a9243a737..13fb8760fe 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_of/dynamic.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_of/dynamic.md @@ -35,7 +35,7 @@ in strict mode. ```py def f(x: type): reveal_type(x) # revealed: type - reveal_type(x.__repr__) # revealed: @Todo(bound method) + reveal_type(x.__repr__) # revealed: class A: ... @@ -50,7 +50,7 @@ x: type = A() # error: [invalid-assignment] ```py def f(x: type[object]): reveal_type(x) # revealed: type - reveal_type(x.__repr__) # revealed: @Todo(bound method) + reveal_type(x.__repr__) # revealed: class A: ... diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/truthiness.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/truthiness.md index 54f5858c98..85186e400b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/truthiness.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/truthiness.md @@ -75,3 +75,19 @@ class Boom: reveal_type(bool(Boom())) # revealed: bool ``` + +### Possibly unbound __bool__ method + +```py +from typing import Literal + +def flag() -> bool: + return True + +class PossiblyUnboundTrue: + if flag(): + def __bool__(self) -> Literal[True]: + return True + +reveal_type(bool(PossiblyUnboundTrue())) # revealed: bool +``` diff --git a/crates/red_knot_python_semantic/src/module_resolver/module.rs b/crates/red_knot_python_semantic/src/module_resolver/module.rs index 09444e64aa..d85a19a23d 100644 --- a/crates/red_knot_python_semantic/src/module_resolver/module.rs +++ b/crates/red_knot_python_semantic/src/module_resolver/module.rs @@ -109,6 +109,7 @@ pub enum KnownModule { #[allow(dead_code)] Abc, // currently only used in tests Collections, + Inspect, KnotExtensions, } @@ -123,6 +124,7 @@ impl KnownModule { Self::Sys => "sys", Self::Abc => "abc", Self::Collections => "collections", + Self::Inspect => "inspect", Self::KnotExtensions => "knot_extensions", } } @@ -149,6 +151,7 @@ impl KnownModule { "sys" => Some(Self::Sys), "abc" => Some(Self::Abc), "collections" => Some(Self::Collections), + "inspect" => Some(Self::Inspect), "knot_extensions" => Some(Self::KnotExtensions), _ => None, } diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 76b55cea27..114761b852 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; @@ -203,6 +204,8 @@ pub enum Type<'db> { Never, /// A specific function object FunctionLiteral(FunctionType<'db>), + /// A callable object + Callable(CallableType<'db>), /// A specific module object ModuleLiteral(ModuleLiteralType<'db>), /// A specific class object @@ -262,6 +265,11 @@ impl<'db> Type<'db> { matches!(self, Type::Never) } + fn is_none(&self, db: &'db dyn Db) -> bool { + self.into_instance() + .is_some_and(|instance| instance.class.is_known(db, KnownClass::NoneType)) + } + pub fn is_object(&self, db: &'db dyn Db) -> bool { self.into_instance() .is_some_and(|instance| instance.class.is_object(db)) @@ -461,6 +469,7 @@ impl<'db> Type<'db> { | Type::Dynamic(_) | Type::Never | Type::FunctionLiteral(_) + | Type::Callable(_) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) | Type::KnownInstance(_) @@ -611,6 +620,21 @@ impl<'db> Type<'db> { .to_instance(db) .is_subtype_of(db, target), + // The same reasoning applies for these special callable types: + (Type::Callable(CallableType::BoundMethod(_)), _) => KnownClass::MethodType + .to_instance(db) + .is_subtype_of(db, target), + (Type::Callable(CallableType::MethodWrapperDunderGet(_)), _) => { + KnownClass::WrapperDescriptorType + .to_instance(db) + .is_subtype_of(db, target) + } + (Type::Callable(CallableType::WrapperDescriptorDunderGet), _) => { + KnownClass::WrapperDescriptorType + .to_instance(db) + .is_subtype_of(db, target) + } + // A fully static heterogenous tuple type `A` is a subtype of a fully static heterogeneous tuple type `B` // iff the two tuple types have the same number of elements and each element-type in `A` is a subtype // of the element-type at the same index in `B`. (Now say that 5 times fast.) @@ -789,6 +813,25 @@ impl<'db> Type<'db> { true } + // TODO: This is a workaround to avoid false positives (e.g. when checking function calls + // with `SupportsIndex` parameters), which should be removed when we understand protocols. + (lhs, Type::Instance(InstanceType { class })) + if class.is_known(db, KnownClass::SupportsIndex) => + { + match lhs { + Type::Instance(InstanceType { class }) + if matches!( + class.known(db), + Some(KnownClass::Int | KnownClass::SupportsIndex) + ) => + { + true + } + Type::IntLiteral(_) => true, + _ => false, + } + } + // TODO other types containing gradual forms (e.g. generics containing Any/Unknown) _ => self.is_subtype_of(db, target), } @@ -909,6 +952,11 @@ impl<'db> Type<'db> { | Type::BytesLiteral(..) | Type::SliceLiteral(..) | Type::FunctionLiteral(..) + | Type::Callable( + CallableType::BoundMethod(..) + | CallableType::MethodWrapperDunderGet(..) + | CallableType::WrapperDescriptorDunderGet, + ) | Type::ModuleLiteral(..) | Type::ClassLiteral(..) | Type::KnownInstance(..)), @@ -918,6 +966,11 @@ impl<'db> Type<'db> { | Type::BytesLiteral(..) | Type::SliceLiteral(..) | Type::FunctionLiteral(..) + | Type::Callable( + CallableType::BoundMethod(..) + | CallableType::MethodWrapperDunderGet(..) + | CallableType::WrapperDescriptorDunderGet, + ) | Type::ModuleLiteral(..) | Type::ClassLiteral(..) | Type::KnownInstance(..)), @@ -932,6 +985,7 @@ impl<'db> Type<'db> { | Type::BooleanLiteral(..) | Type::BytesLiteral(..) | Type::FunctionLiteral(..) + | Type::Callable(..) | Type::IntLiteral(..) | Type::SliceLiteral(..) | Type::StringLiteral(..) @@ -943,6 +997,7 @@ impl<'db> Type<'db> { | Type::BooleanLiteral(..) | Type::BytesLiteral(..) | Type::FunctionLiteral(..) + | Type::Callable(..) | Type::IntLiteral(..) | Type::SliceLiteral(..) | Type::StringLiteral(..) @@ -971,6 +1026,7 @@ impl<'db> Type<'db> { | Type::BytesLiteral(..) | Type::SliceLiteral(..) | Type::FunctionLiteral(..) + | Type::Callable(..) | Type::ModuleLiteral(..), ) | ( @@ -981,6 +1037,7 @@ impl<'db> Type<'db> { | Type::BytesLiteral(..) | Type::SliceLiteral(..) | Type::FunctionLiteral(..) + | Type::Callable(..) | Type::ModuleLiteral(..), Type::SubclassOf(_), ) => true, @@ -1085,6 +1142,33 @@ impl<'db> Type<'db> { !KnownClass::FunctionType.is_subclass_of(db, class) } + ( + Type::Callable(CallableType::BoundMethod(_)), + Type::Instance(InstanceType { class }), + ) + | ( + Type::Instance(InstanceType { class }), + Type::Callable(CallableType::BoundMethod(_)), + ) => !KnownClass::MethodType.is_subclass_of(db, class), + + ( + Type::Callable(CallableType::MethodWrapperDunderGet(_)), + Type::Instance(InstanceType { class }), + ) + | ( + Type::Instance(InstanceType { class }), + Type::Callable(CallableType::MethodWrapperDunderGet(_)), + ) => !KnownClass::MethodWrapperType.is_subclass_of(db, class), + + ( + Type::Callable(CallableType::WrapperDescriptorDunderGet), + Type::Instance(InstanceType { class }), + ) + | ( + Type::Instance(InstanceType { class }), + Type::Callable(CallableType::WrapperDescriptorDunderGet), + ) => !KnownClass::WrapperDescriptorType.is_subclass_of(db, class), + (Type::ModuleLiteral(..), other @ Type::Instance(..)) | (other @ Type::Instance(..), Type::ModuleLiteral(..)) => { // Modules *can* actually be instances of `ModuleType` subclasses @@ -1130,6 +1214,11 @@ impl<'db> Type<'db> { Type::Dynamic(_) => false, Type::Never | Type::FunctionLiteral(..) + | Type::Callable( + CallableType::BoundMethod(_) + | CallableType::MethodWrapperDunderGet(_) + | CallableType::WrapperDescriptorDunderGet, + ) | Type::ModuleLiteral(..) | Type::IntLiteral(_) | Type::BooleanLiteral(_) @@ -1194,6 +1283,11 @@ impl<'db> Type<'db> { Type::SubclassOf(..) => false, Type::BooleanLiteral(_) | Type::FunctionLiteral(..) + | Type::Callable( + CallableType::BoundMethod(_) + | CallableType::MethodWrapperDunderGet(_) + | CallableType::WrapperDescriptorDunderGet, + ) | Type::ClassLiteral(..) | Type::ModuleLiteral(..) | Type::KnownInstance(..) => true, @@ -1232,6 +1326,11 @@ impl<'db> Type<'db> { pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool { match self { Type::FunctionLiteral(..) + | Type::Callable( + CallableType::BoundMethod(..) + | CallableType::MethodWrapperDunderGet(..) + | CallableType::WrapperDescriptorDunderGet, + ) | Type::ModuleLiteral(..) | Type::ClassLiteral(..) | Type::IntLiteral(..) @@ -1274,18 +1373,23 @@ impl<'db> Type<'db> { | KnownClass::FrozenSet | KnownClass::Dict | KnownClass::Slice + | KnownClass::Range | KnownClass::Property | KnownClass::BaseException | KnownClass::BaseExceptionGroup | KnownClass::GenericAlias | KnownClass::ModuleType | KnownClass::FunctionType + | KnownClass::MethodType + | KnownClass::MethodWrapperType + | KnownClass::WrapperDescriptorType | KnownClass::SpecialForm | KnownClass::ChainMap | KnownClass::Counter | KnownClass::DefaultDict | KnownClass::Deque | KnownClass::OrderedDict + | KnownClass::SupportsIndex | KnownClass::StdlibAlias | KnownClass::TypeVar, ) => false, @@ -1302,35 +1406,42 @@ impl<'db> Type<'db> { } } - /// Resolve a member access of a type. + /// Access an attribute of this type without invoking the descriptor protocol. This + /// method corresponds to `inspect.getattr_static(, name)`. /// - /// For example, if `foo` is `Type::Instance()`, - /// `foo.member(&db, "baz")` returns the type of `baz` attributes - /// as accessed from instances of the `Bar` class. + /// See also: [`Type::member`] #[must_use] - pub(crate) fn member(&self, db: &'db dyn Db, name: &str) -> Symbol<'db> { - if name == "__class__" { - return Symbol::bound(self.to_meta_type(db)); - } - + fn static_member(&self, db: &'db dyn Db, name: &str) -> Symbol<'db> { match self { Type::Dynamic(_) => Symbol::bound(self), Type::Never => Symbol::todo("attribute lookup on Never"), - Type::FunctionLiteral(_) => match name { - "__get__" => Symbol::todo("`__get__` method on functions"), - "__call__" => Symbol::todo("`__call__` method on functions"), - _ => KnownClass::FunctionType.to_instance(db).member(db, name), - }, + Type::FunctionLiteral(_) => KnownClass::FunctionType + .to_instance(db) + .static_member(db, name), - Type::ModuleLiteral(module) => module.member(db, name), + Type::Callable(CallableType::BoundMethod(_)) => KnownClass::MethodType + .to_instance(db) + .static_member(db, name), + Type::Callable(CallableType::MethodWrapperDunderGet(_)) => { + KnownClass::MethodWrapperType + .to_instance(db) + .static_member(db, name) + } + Type::Callable(CallableType::WrapperDescriptorDunderGet) => { + KnownClass::WrapperDescriptorType + .to_instance(db) + .static_member(db, name) + } - Type::ClassLiteral(class_ty) => class_ty.member(db, name), + Type::ModuleLiteral(module) => module.static_member(db, name), - Type::SubclassOf(subclass_of_ty) => subclass_of_ty.member(db, name), + Type::ClassLiteral(class_ty) => class_ty.static_member(db, name), - Type::KnownInstance(known_instance) => known_instance.member(db, name), + Type::SubclassOf(subclass_of_ty) => subclass_of_ty.static_member(db, name), + + Type::KnownInstance(known_instance) => known_instance.static_member(db, name), Type::Instance(InstanceType { class }) => match (class.known(db), name) { (Some(KnownClass::VersionInfo), "major") => Symbol::bound(Type::IntLiteral( @@ -1339,87 +1450,68 @@ impl<'db> Type<'db> { (Some(KnownClass::VersionInfo), "minor") => Symbol::bound(Type::IntLiteral( Program::get(db).python_version(db).minor.into(), )), + (Some(KnownClass::FunctionType), "__get__") => { + Symbol::bound(Type::Callable(CallableType::WrapperDescriptorDunderGet)) + } + + // TODO: + // We currently hard-code the knowledge that the following known classes are not + // descriptors, i.e. that they have no `__get__` method. This is not wrong and + // potentially even beneficial for performance, but it's not very principled. + // This case can probably be removed eventually, but we include it at the moment + // because we make extensive use of these types in our test suite. Note that some + // builtin types are not included here, since they do not have generic bases and + // are correctly handled by the `instance_member` method. + ( + Some( + KnownClass::Str + | KnownClass::Bytes + | KnownClass::Tuple + | KnownClass::Slice + | KnownClass::Range, + ), + "__get__", + ) => Symbol::Unbound, + _ => { let SymbolAndQualifiers(symbol, _) = class.instance_member(db, name); symbol } }, - Type::Union(union) => { - let mut builder = UnionBuilder::new(db); + Type::Union(union) => union.map_with_boundness(db, |elem| elem.static_member(db, name)), - let mut all_unbound = true; - let mut possibly_unbound = false; - for ty in union.elements(db) { - let ty_member = ty.member(db, name); - match ty_member { - Symbol::Unbound => { - possibly_unbound = true; - } - Symbol::Type(ty_member, member_boundness) => { - if member_boundness == Boundness::PossiblyUnbound { - possibly_unbound = true; - } - - all_unbound = false; - builder = builder.add(ty_member); - } - } - } - - if all_unbound { - Symbol::Unbound - } else { - Symbol::Type( - builder.build(), - if possibly_unbound { - Boundness::PossiblyUnbound - } else { - Boundness::Bound - }, - ) - } - } - - Type::Intersection(_) => { - // TODO perform the get_member on each type in the intersection - // TODO return the intersection of those results - Symbol::todo("Attribute access on `Intersection` types") + Type::Intersection(intersection) => { + intersection.map_with_boundness(db, |elem| elem.static_member(db, name)) } Type::IntLiteral(_) => match name { "real" | "numerator" => Symbol::bound(self), // TODO more attributes could probably be usefully special-cased - _ => KnownClass::Int.to_instance(db).member(db, name), + _ => KnownClass::Int.to_instance(db).static_member(db, name), }, Type::BooleanLiteral(bool_value) => match name { "real" | "numerator" => Symbol::bound(Type::IntLiteral(i64::from(*bool_value))), - _ => KnownClass::Bool.to_instance(db).member(db, name), + _ => KnownClass::Bool.to_instance(db).static_member(db, name), }, - Type::StringLiteral(_) => { - // TODO defer to `typing.LiteralString`/`builtins.str` methods - // from typeshed's stubs - Symbol::todo("Attribute access on `StringLiteral` types") + Type::StringLiteral(_) | Type::LiteralString => { + KnownClass::Str.to_instance(db).static_member(db, name) } - Type::LiteralString => { - // TODO defer to `typing.LiteralString`/`builtins.str` methods - // from typeshed's stubs - Symbol::todo("Attribute access on `LiteralString` types") - } - - Type::BytesLiteral(_) => KnownClass::Bytes.to_instance(db).member(db, name), + Type::BytesLiteral(_) => KnownClass::Bytes.to_instance(db).static_member(db, name), // We could plausibly special-case `start`, `step`, and `stop` here, // but it doesn't seem worth the complexity given the very narrow range of places // where we infer `SliceLiteral` types. - Type::SliceLiteral(_) => KnownClass::Slice.to_instance(db).member(db, name), + Type::SliceLiteral(_) => KnownClass::Slice.to_instance(db).static_member(db, name), Type::Tuple(_) => { - // TODO: implement tuple methods - Symbol::todo("Attribute access on heterogeneous tuple types") + // TODO: We might want to special case some attributes here, as the stubs + // for `builtins.tuple` assume that `self` is a homogeneous tuple, while + // we're explicitly modeling heterogeneous tuples using `Type::Tuple`. + KnownClass::Tuple.to_instance(db).static_member(db, name) } Type::AlwaysTruthy | Type::AlwaysFalsy => match name { @@ -1427,11 +1519,168 @@ impl<'db> Type<'db> { // TODO should be `Callable[[], Literal[True/False]]` Symbol::todo("`__bool__` for `AlwaysTruthy`/`AlwaysFalsy` Type variants") } - _ => Type::object(db).member(db, name), + _ => Type::object(db).static_member(db, name), }, } } + /// Call the `__get__(instance, owner)` method on a type, and get the return + /// type of the call. + /// + /// If `__get__` is not defined on the type, this method returns `Ok(None)`. + /// If the call to `__get__` fails, this method returns an error. + fn try_call_dunder_get( + self, + db: &'db dyn Db, + instance: Option>, + owner: Type<'db>, + ) -> Option> { + #[salsa::tracked] + fn try_call_dunder_get_query<'db>( + db: &'db dyn Db, + ty_self: Type<'db>, + instance: Option>, + owner: Type<'db>, + ) -> Option> { + // TODO: Handle possible-unboundness and errors from `__get__` calls. + + match ty_self { + Type::Union(union) => { + let mut builder = UnionBuilder::new(db); + for elem in union.elements(db) { + let ty = if let Some(result) = elem.try_call_dunder_get(db, instance, owner) + { + result + } else { + *elem + }; + builder = builder.add(ty); + } + Some(builder.build()) + } + Type::Intersection(intersection) => { + if !intersection.negative(db).is_empty() { + return Some(todo_type!( + "try_call_dunder_get: intersections with negative contributions" + )); + } + + let mut builder = IntersectionBuilder::new(db); + for elem in intersection.positive(db) { + let ty = if let Some(result) = elem.try_call_dunder_get(db, instance, owner) + { + result + } else { + *elem + }; + builder = builder.add_positive(ty); + } + Some(builder.build()) + } + _ => { + // TODO: Handle possible-unboundness of `__get__` method + // There is an existing test case for this in `descriptor_protocol.md`. + + ty_self + .member(db, "__get__") + .ignore_possibly_unbound()? + .try_call( + db, + &CallArguments::positional([instance.unwrap_or(Type::none(db)), owner]), + ) + .map(|outcome| Some(outcome.return_type(db))) + .unwrap_or(None) + } + } + } + + try_call_dunder_get_query(db, self, instance, owner) + } + + /// Access an attribute of this type, potentially invoking the descriptor protocol. + /// Corresponds to `getattr(, name)`. + /// + /// See also: [`Type::static_member`] + /// + /// TODO: We should return a `Result` here to handle errors that can appear during attribute + /// lookup, like a failed `__get__` call on a descriptor. + #[must_use] + pub(crate) fn member(&self, db: &'db dyn Db, name: &str) -> Symbol<'db> { + if name == "__class__" { + return Symbol::bound(self.to_meta_type(db)); + } + + match self { + Type::FunctionLiteral(function) if name == "__get__" => Symbol::bound(Type::Callable( + CallableType::MethodWrapperDunderGet(*function), + )), + + Type::Callable(CallableType::BoundMethod(bound_method)) => match name { + "__self__" => Symbol::bound(bound_method.self_instance(db)), + "__func__" => Symbol::bound(Type::FunctionLiteral(bound_method.function(db))), + _ => { + KnownClass::MethodType + .to_instance(db) + .member(db, name) + .or_fall_back_to(db, || { + // If an attribute is not available on the bound method object, + // it will be looked up on the underlying function object: + Type::FunctionLiteral(bound_method.function(db)).member(db, name) + }) + } + }, + Type::Callable(CallableType::MethodWrapperDunderGet(_)) => { + KnownClass::MethodWrapperType + .to_instance(db) + .member(db, name) + } + Type::Callable(CallableType::WrapperDescriptorDunderGet) => { + KnownClass::WrapperDescriptorType + .to_instance(db) + .member(db, name) + } + + Type::Instance(..) + | Type::BooleanLiteral(..) + | Type::IntLiteral(..) + | Type::StringLiteral(..) + | Type::BytesLiteral(..) + | Type::LiteralString + | Type::SliceLiteral(..) + | Type::Tuple(..) + | Type::KnownInstance(..) + | Type::FunctionLiteral(..) => { + let member = self.static_member(db, name); + + let instance = Some(*self); + let owner = self.to_meta_type(db); + + // TODO: Handle `__get__` call errors instead of using `.unwrap_or(None)`. + // There is an existing test case for this in `descriptor_protocol.md`. + member.map_type(|ty| ty.try_call_dunder_get(db, instance, owner).unwrap_or(ty)) + } + Type::ClassLiteral(..) | Type::SubclassOf(..) => { + let member = self.static_member(db, name); + + let instance = None; + let owner = self.to_meta_type(db); + + // TODO: Handle `__get__` call errors (see above). + member.map_type(|ty| ty.try_call_dunder_get(db, instance, owner).unwrap_or(ty)) + } + Type::Union(union) => union.map_with_boundness(db, |elem| elem.member(db, name)), + Type::Intersection(intersection) => { + intersection.map_with_boundness(db, |elem| elem.member(db, name)) + } + + Type::Dynamic(..) + | Type::Never + | Type::AlwaysFalsy + | Type::AlwaysTruthy + | Type::ModuleLiteral(..) => self.static_member(db, name), + } + } + /// Resolves the boolean value of a type. /// /// This is used to determine the value that would be returned @@ -1440,6 +1689,7 @@ impl<'db> Type<'db> { match self { Type::Dynamic(_) | Type::Never => Truthiness::Ambiguous, Type::FunctionLiteral(_) => Truthiness::AlwaysTrue, + Type::Callable(_) => Truthiness::AlwaysTrue, Type::ModuleLiteral(_) => Truthiness::AlwaysTrue, Type::ClassLiteral(ClassLiteralType { class }) => { class.metaclass(db).to_instance(db).bool(db) @@ -1451,7 +1701,7 @@ impl<'db> Type<'db> { .unwrap_or(Truthiness::Ambiguous), Type::AlwaysTruthy => Truthiness::AlwaysTrue, Type::AlwaysFalsy => Truthiness::AlwaysFalse, - instance_ty @ Type::Instance(InstanceType { class }) => { + Type::Instance(InstanceType { class }) => { if class.is_known(db, KnownClass::Bool) { Truthiness::Ambiguous } else if class.is_known(db, KnownClass::NoneType) { @@ -1459,18 +1709,10 @@ impl<'db> Type<'db> { } else { // We only check the `__bool__` method for truth testing, even though at // runtime there is a fallback to `__len__`, since `__bool__` takes precedence - // and a subclass could add a `__bool__` method. We don't use - // `Type::call_dunder` here because of the need to check for `__bool__ = bool`. + // and a subclass could add a `__bool__` method. - // Don't trust a maybe-unbound `__bool__` method. - let Symbol::Type(bool_method, Boundness::Bound) = - instance_ty.to_meta_type(db).member(db, "__bool__") - else { - return Truthiness::Ambiguous; - }; - - if let Ok(Type::BooleanLiteral(bool_val)) = bool_method - .try_call_bound(db, instance_ty, &CallArguments::positional([])) + if let Ok(Type::BooleanLiteral(bool_val)) = self + .try_call_dunder(db, "__bool__", &CallArguments::none()) .map(|outcome| outcome.return_type(db)) { bool_val.into() @@ -1543,15 +1785,12 @@ impl<'db> Type<'db> { return usize_len.try_into().ok().map(Type::IntLiteral); } - let return_ty = - match self.try_call_dunder(db, "__len__", &CallArguments::positional([*self])) { - Ok(outcome) | Err(CallDunderError::PossiblyUnbound(outcome)) => { - outcome.return_type(db) - } + let return_ty = match self.try_call_dunder(db, "__len__", &CallArguments::none()) { + Ok(outcome) | Err(CallDunderError::PossiblyUnbound(outcome)) => outcome.return_type(db), - // TODO: emit a diagnostic - Err(err) => err.return_type(db)?, - }; + // TODO: emit a diagnostic + Err(err) => err.return_type(db)?, + }; non_negative_int_literal(db, return_ty) } @@ -1565,8 +1804,153 @@ impl<'db> Type<'db> { arguments: &CallArguments<'_, 'db>, ) -> Result, CallError<'db>> { match self { + Type::Callable(CallableType::BoundMethod(bound_method)) => { + let instance = bound_method.self_instance(db); + let arguments = arguments.with_self(instance); + + let binding = bind_call( + db, + &arguments, + bound_method.function(db).signature(db), + self, + ); + + if binding.has_binding_errors() { + Err(CallError::BindingError { binding }) + } else { + Ok(CallOutcome::Single(binding)) + } + } + Type::Callable(CallableType::MethodWrapperDunderGet(function)) => { + // 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) => { + // 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().is_some_and(|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); + + if binding.has_binding_errors() { + return Err(CallError::BindingError { binding }); + } + match function_type.known(db) { Some(KnownFunction::IsEquivalentTo) => { let (ty_a, ty_b) = binding @@ -1640,6 +2024,48 @@ impl<'db> Type<'db> { }; } + Some(KnownFunction::GetattrStatic) => { + let Some((instance_ty, attr_name, default)) = + binding.three_parameter_types() + else { + return Ok(CallOutcome::Single(binding)); + }; + + let Some(attr_name) = attr_name.into_string_literal() else { + return Ok(CallOutcome::Single(binding)); + }; + + let default = if default.is_unknown() { + Type::Never + } else { + default + }; + + let union_with_default = |ty| UnionType::from_elements(db, [ty, default]); + + // TODO: we could emit a diagnostic here (if default is not set) + binding.set_return_type( + match instance_ty.static_member(db, attr_name.value(db)) { + Symbol::Type(ty, Boundness::Bound) => { + if instance_ty.is_fully_static(db) { + ty + } else { + // Here, we attempt to model the fact that an attribute lookup on + // a non-fully static type could fail. This is an approximation, + // as there are gradual types like `tuple[Any]`, on which a lookup + // of (e.g. of the `index` method) would always succeed. + + union_with_default(ty) + } + } + Symbol::Type(ty, Boundness::PossiblyUnbound) => { + union_with_default(ty) + } + Symbol::Unbound => default, + }, + ); + } + _ => {} }; @@ -1677,7 +2103,7 @@ impl<'db> Type<'db> { instance_ty @ Type::Instance(_) => { instance_ty - .try_call_dunder(db, "__call__", &arguments.with_self(instance_ty)) + .try_call_dunder(db, "__call__", arguments) .map_err(|err| match err { CallDunderError::Call(CallError::NotCallable { .. }) => { // Turn "`` not callable" into @@ -1742,8 +2168,6 @@ impl<'db> Type<'db> { receiver_ty: &Type<'db>, arguments: &CallArguments<'_, 'db>, ) -> Result, CallError<'db>> { - debug_assert!(receiver_ty.is_instance() || receiver_ty.is_class_literal()); - match self { Type::FunctionLiteral(..) => { // Functions are always descriptors, so this would effectively call @@ -1751,10 +2175,7 @@ impl<'db> Type<'db> { self.try_call(db, &arguments.with_self(*receiver_ty)) } - Type::Instance(_) | Type::ClassLiteral(_) => { - // TODO descriptor protocol. For now, assume non-descriptor and call without `self` argument. - self.try_call(db, arguments) - } + Type::Instance(_) | Type::ClassLiteral(_) => self.try_call(db, arguments), Type::Union(union) => CallOutcome::try_call_union(db, union, |element| { element.try_call_bound(db, receiver_ty, arguments) @@ -1781,9 +2202,11 @@ impl<'db> Type<'db> { arguments: &CallArguments<'_, 'db>, ) -> Result, CallDunderError<'db>> { match self.to_meta_type(db).member(db, name) { - Symbol::Type(callable_ty, Boundness::Bound) => Ok(callable_ty.try_call(db, arguments)?), + Symbol::Type(callable_ty, Boundness::Bound) => { + Ok(callable_ty.try_call_bound(db, &self, arguments)?) + } Symbol::Type(callable_ty, Boundness::PossiblyUnbound) => { - let call = callable_ty.try_call(db, arguments)?; + let call = callable_ty.try_call_bound(db, &self, arguments)?; Err(CallDunderError::PossiblyUnbound(call)) } Symbol::Unbound => Err(CallDunderError::MethodNotAvailable), @@ -1805,17 +2228,12 @@ impl<'db> Type<'db> { }; } - let dunder_iter_result = - self.try_call_dunder(db, "__iter__", &CallArguments::positional([self])); + let dunder_iter_result = self.try_call_dunder(db, "__iter__", &CallArguments::none()); match &dunder_iter_result { Ok(outcome) | Err(CallDunderError::PossiblyUnbound(outcome)) => { let iterator_ty = outcome.return_type(db); - return match iterator_ty.try_call_dunder( - db, - "__next__", - &CallArguments::positional([iterator_ty]), - ) { + return match iterator_ty.try_call_dunder(db, "__next__", &CallArguments::none()) { Ok(outcome) => { if matches!( dunder_iter_result, @@ -1863,7 +2281,7 @@ impl<'db> Type<'db> { match self.try_call_dunder( db, "__getitem__", - &CallArguments::positional([self, KnownClass::Int.to_instance(db)]), + &CallArguments::positional([KnownClass::Int.to_instance(db)]), ) { Ok(outcome) => IterationOutcome::Iterable { element_ty: outcome.return_type(db), @@ -1897,6 +2315,7 @@ impl<'db> Type<'db> { Type::BooleanLiteral(_) | Type::BytesLiteral(_) | Type::FunctionLiteral(_) + | Type::Callable(..) | Type::Instance(_) | Type::KnownInstance(_) | Type::ModuleLiteral(_) @@ -2092,6 +2511,15 @@ impl<'db> Type<'db> { Type::SliceLiteral(_) => KnownClass::Slice.to_class_literal(db), Type::IntLiteral(_) => KnownClass::Int.to_class_literal(db), Type::FunctionLiteral(_) => KnownClass::FunctionType.to_class_literal(db), + Type::Callable(CallableType::BoundMethod(_)) => { + KnownClass::MethodType.to_class_literal(db) + } + Type::Callable(CallableType::MethodWrapperDunderGet(_)) => { + KnownClass::MethodWrapperType.to_class_literal(db) + } + Type::Callable(CallableType::WrapperDescriptorDunderGet) => { + KnownClass::WrapperDescriptorType.to_class_literal(db) + } Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db), Type::Tuple(_) => KnownClass::Tuple.to_class_literal(db), Type::ClassLiteral(ClassLiteralType { class }) => class.metaclass(db), @@ -2325,6 +2753,7 @@ pub enum KnownClass { FrozenSet, Dict, Slice, + Range, Property, BaseException, BaseExceptionGroup, @@ -2332,6 +2761,9 @@ pub enum KnownClass { GenericAlias, ModuleType, FunctionType, + MethodType, + MethodWrapperType, + WrapperDescriptorType, // Typeshed NoneType, // Part of `types` for Python >= 3.10 // Typing @@ -2340,6 +2772,8 @@ pub enum KnownClass { TypeVar, TypeAliasType, NoDefaultType, + // TODO: This can probably be removed when we have support for protocols + SupportsIndex, // Collections ChainMap, Counter, @@ -2374,17 +2808,22 @@ impl<'db> KnownClass { Self::List => "list", Self::Type => "type", Self::Slice => "slice", + Self::Range => "range", Self::Property => "property", Self::BaseException => "BaseException", Self::BaseExceptionGroup => "BaseExceptionGroup", Self::GenericAlias => "GenericAlias", Self::ModuleType => "ModuleType", Self::FunctionType => "FunctionType", + Self::MethodType => "MethodType", + Self::MethodWrapperType => "MethodWrapperType", + Self::WrapperDescriptorType => "WrapperDescriptorType", Self::NoneType => "NoneType", Self::SpecialForm => "_SpecialForm", Self::TypeVar => "TypeVar", Self::TypeAliasType => "TypeAliasType", Self::NoDefaultType => "_NoDefaultType", + Self::SupportsIndex => "SupportsIndex", Self::ChainMap => "ChainMap", Self::Counter => "Counter", Self::DefaultDict => "defaultdict", @@ -2455,13 +2894,21 @@ impl<'db> KnownClass { | Self::BaseException | Self::BaseExceptionGroup | Self::Slice + | Self::Range | Self::Property => KnownModule::Builtins, Self::VersionInfo => KnownModule::Sys, - Self::GenericAlias | Self::ModuleType | Self::FunctionType => KnownModule::Types, + Self::GenericAlias + | Self::ModuleType + | Self::FunctionType + | Self::MethodType + | Self::MethodWrapperType + | Self::WrapperDescriptorType => KnownModule::Types, Self::NoneType => KnownModule::Typeshed, - Self::SpecialForm | Self::TypeVar | Self::TypeAliasType | Self::StdlibAlias => { - KnownModule::Typing - } + Self::SpecialForm + | Self::TypeVar + | Self::TypeAliasType + | Self::StdlibAlias + | Self::SupportsIndex => KnownModule::Typing, Self::NoDefaultType => { let python_version = Program::get(db).python_version(db); @@ -2517,10 +2964,14 @@ impl<'db> KnownClass { | Self::List | Self::Type | Self::Slice + | Self::Range | Self::Property | Self::GenericAlias | Self::ModuleType | Self::FunctionType + | Self::MethodType + | Self::MethodWrapperType + | Self::WrapperDescriptorType | Self::SpecialForm | Self::ChainMap | Self::Counter @@ -2528,6 +2979,7 @@ impl<'db> KnownClass { | Self::Deque | Self::OrderedDict | Self::StdlibAlias + | Self::SupportsIndex | Self::BaseException | Self::BaseExceptionGroup | Self::TypeVar => false, @@ -2553,12 +3005,16 @@ impl<'db> KnownClass { "dict" => Self::Dict, "list" => Self::List, "slice" => Self::Slice, + "range" => Self::Range, "BaseException" => Self::BaseException, "BaseExceptionGroup" => Self::BaseExceptionGroup, "GenericAlias" => Self::GenericAlias, "NoneType" => Self::NoneType, "ModuleType" => Self::ModuleType, "FunctionType" => Self::FunctionType, + "MethodType" => Self::MethodType, + "MethodWrapperType" => Self::MethodWrapperType, + "WrapperDescriptorType" => Self::WrapperDescriptorType, "TypeAliasType" => Self::TypeAliasType, "ChainMap" => Self::ChainMap, "Counter" => Self::Counter, @@ -2568,6 +3024,7 @@ impl<'db> KnownClass { "_Alias" => Self::StdlibAlias, "_SpecialForm" => Self::SpecialForm, "_NoDefaultType" => Self::NoDefaultType, + "SupportsIndex" => Self::SupportsIndex, "_version_info" => Self::VersionInfo, "ellipsis" if Program::get(db).python_version(db) <= PythonVersion::PY39 => { Self::EllipsisType @@ -2600,6 +3057,7 @@ impl<'db> KnownClass { | Self::FrozenSet | Self::Dict | Self::Slice + | Self::Range | Self::Property | Self::GenericAlias | Self::ChainMap @@ -2613,9 +3071,12 @@ impl<'db> KnownClass { | Self::BaseException | Self::EllipsisType | Self::BaseExceptionGroup - | Self::FunctionType => module == self.canonical_module(db), + | Self::FunctionType + | Self::MethodType + | Self::MethodWrapperType + | Self::WrapperDescriptorType => module == self.canonical_module(db), Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types), - Self::SpecialForm | Self::TypeVar | Self::TypeAliasType | Self::NoDefaultType => { + Self::SpecialForm | Self::TypeVar | Self::TypeAliasType | Self::NoDefaultType | Self::SupportsIndex => { matches!(module, KnownModule::Typing | KnownModule::TypingExtensions) } } @@ -2948,11 +3409,11 @@ impl<'db> KnownInstanceType<'db> { } } - fn member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { + fn static_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { let ty = match (self, name) { (Self::TypeVar(typevar), "__name__") => Type::string_literal(db, typevar.name(db)), (Self::TypeAliasType(alias), "__name__") => Type::string_literal(db, alias.name(db)), - _ => return self.instance_fallback(db).member(db, name), + _ => return self.instance_fallback(db).static_member(db, name), }; Symbol::bound(ty) } @@ -3199,6 +3660,9 @@ pub enum KnownFunction { /// `typing(_extensions).cast` Cast, + /// `inspect.getattr_static` + GetattrStatic, + /// `knot_extensions.static_assert` StaticAssert, /// `knot_extensions.is_equivalent_to` @@ -3242,6 +3706,7 @@ impl KnownFunction { "no_type_check" => Self::NoTypeCheck, "assert_type" => Self::AssertType, "cast" => Self::Cast, + "getattr_static" => Self::GetattrStatic, "static_assert" => Self::StaticAssert, "is_subtype_of" => Self::IsSubtypeOf, "is_disjoint_from" => Self::IsDisjointFrom, @@ -3271,6 +3736,9 @@ impl KnownFunction { Self::AssertType | Self::Cast | Self::RevealType | Self::Final | Self::NoTypeCheck => { matches!(module, KnownModule::Typing | KnownModule::TypingExtensions) } + Self::GetattrStatic => { + matches!(module, KnownModule::Inspect) + } Self::IsAssignableTo | Self::IsDisjointFrom | Self::IsEquivalentTo @@ -3305,11 +3773,62 @@ impl KnownFunction { | Self::Final | Self::NoTypeCheck | Self::RevealType + | Self::GetattrStatic | Self::StaticAssert => ParameterExpectations::AllValueExpressions, } } } +/// This type represents bound method objects that are created when a method is accessed +/// on an instance of a class. For example, the expression `Path("a.txt").touch` creates +/// a bound method object that represents the `Path.touch` method which is bound to the +/// instance `Path("a.txt")`. +#[salsa::tracked] +pub struct BoundMethodType<'db> { + /// The function that is being bound. Corresponds to the `__func__` attribute on a + /// bound method object + pub(crate) function: FunctionType<'db>, + /// The instance on which this method has been called. Corresponds to the `__self__` + /// attribute on a bound method object + self_instance: Type<'db>, +} + +/// A type that represents callable objects. +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update)] +pub enum CallableType<'db> { + /// Represents a callable `instance.method` where `instance` is an instance of a class + /// and `method` is a method (of that class). + /// + /// See [`BoundMethodType`] for more information. + /// + /// TODO: This could eventually be replaced by a more general `Callable` type, if we + /// decide to bind the first argument of method calls early, i.e. if we have a method + /// `def f(self, x: int) -> str`, and see it being called as `instance.f`, we could + /// partially apply (and check) the `instance` argument against the `self` parameter, + /// and return a `Callable[[int], str]`. One drawback would be that we could not show + /// the bound instance when that type is displayed. + BoundMethod(BoundMethodType<'db>), + + /// Represents the callable `f.__get__` where `f` is a function. + /// + /// TODO: This could eventually be replaced by a more general `Callable` type that is + /// also able to represent overloads. It would need to represent the two overloads of + /// `types.FunctionType.__get__`: + /// + /// ```txt + /// * (None, type) -> Literal[function_on_which_it_was_called] + /// * (object, type | None) -> BoundMethod[instance, function_on_which_it_was_called] + /// ``` + MethodWrapperDunderGet(FunctionType<'db>), + + /// Represents the callable `FunctionType.__get__`. + /// + /// TODO: Similar to above, this could eventually be replaced by a generic `Callable` + /// type. We currently add this as a separate variant because `FunctionType.__get__` + /// is an overloaded method and we do not support `@overload` yet. + WrapperDescriptorDunderGet, +} + /// Describes whether the parameters in a function expect value expressions or type expressions. /// /// Whether a specific parameter in the function expects a type expression can be queried @@ -3386,14 +3905,14 @@ pub struct ModuleLiteralType<'db> { } impl<'db> ModuleLiteralType<'db> { - fn member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { + fn static_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { // `__dict__` is a very special member that is never overridden by module globals; // we should always look it up directly as an attribute on `types.ModuleType`, // never in the global scope of the module. if name == "__dict__" { return KnownClass::ModuleType .to_instance(db) - .member(db, "__dict__"); + .static_member(db, "__dict__"); } // If the file that originally imported the module has also imported a submodule @@ -3904,7 +4423,6 @@ impl<'db> Class<'db> { // - `typing.Final` // - Proper diagnostics // - Handling of possibly-undeclared/possibly-unbound attributes - // - The descriptor protocol let body_scope = self.body_scope(db); let table = symbol_table(db, body_scope); @@ -3924,8 +4442,10 @@ impl<'db> Class<'db> { // and non-property methods. if function.has_decorator(db, KnownClass::Property.to_class_literal(db)) { SymbolAndQualifiers::todo("@property") + } else if !function.decorators(db).is_empty() { + SymbolAndQualifiers::todo("decorated method") } else { - SymbolAndQualifiers::todo("bound method") + SymbolAndQualifiers(Symbol::bound(declared_ty), qualifiers) } } else { SymbolAndQualifiers(Symbol::bound(declared_ty), qualifiers) @@ -4043,7 +4563,7 @@ impl<'db> ClassLiteralType<'db> { self.class.body_scope(db) } - fn member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { + fn static_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { self.class.class_member(db, name) } } @@ -4146,6 +4666,46 @@ impl<'db> UnionType<'db> { Self::from_elements(db, self.elements(db).iter().map(transform_fn)) } + pub(crate) fn map_with_boundness( + self, + db: &'db dyn Db, + mut transform_fn: impl FnMut(&Type<'db>) -> Symbol<'db>, + ) -> Symbol<'db> { + let mut builder = UnionBuilder::new(db); + + let mut all_unbound = true; + let mut possibly_unbound = false; + for ty in self.elements(db) { + let ty_member = transform_fn(ty); + match ty_member { + Symbol::Unbound => { + possibly_unbound = true; + } + Symbol::Type(ty_member, member_boundness) => { + if member_boundness == Boundness::PossiblyUnbound { + possibly_unbound = true; + } + + all_unbound = false; + builder = builder.add(ty_member); + } + } + } + + if all_unbound { + Symbol::Unbound + } else { + Symbol::Type( + builder.build(), + if possibly_unbound { + Boundness::PossiblyUnbound + } else { + Boundness::Bound + }, + ) + } + } + pub fn is_fully_static(self, db: &'db dyn Db) -> bool { self.elements(db).iter().all(|ty| ty.is_fully_static(db)) } @@ -4368,6 +4928,49 @@ impl<'db> IntersectionType<'db> { .zip(sorted_other.negative(db)) .all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty)) } + + pub(crate) fn map_with_boundness( + self, + db: &'db dyn Db, + mut transform_fn: impl FnMut(&Type<'db>) -> Symbol<'db>, + ) -> Symbol<'db> { + if !self.negative(db).is_empty() { + return Symbol::todo("map_with_boundness: intersections with negative contributions"); + } + + let mut builder = IntersectionBuilder::new(db); + + let mut any_unbound = false; + let mut any_possibly_unbound = false; + for ty in self.positive(db) { + let ty_member = transform_fn(ty); + match ty_member { + Symbol::Unbound => { + any_unbound = true; + } + Symbol::Type(ty_member, member_boundness) => { + if member_boundness == Boundness::PossiblyUnbound { + any_possibly_unbound = true; + } + + builder = builder.add_positive(ty_member); + } + } + } + + if any_unbound { + Symbol::Unbound + } else { + Symbol::Type( + builder.build(), + if any_possibly_unbound { + Boundness::PossiblyUnbound + } else { + Boundness::Bound + }, + ) + } + } } #[salsa::interned] 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..8aaeb4e0fa 100644 --- a/crates/red_knot_python_semantic/src/types/call/arguments.rs +++ b/crates/red_knot_python_semantic/src/types/call/arguments.rs @@ -5,6 +5,11 @@ use super::Type; pub(crate) struct CallArguments<'a, 'db>(Vec>); impl<'a, 'db> CallArguments<'a, 'db> { + /// Create a [`CallArguments`] with no arguments. + pub(crate) fn none() -> Self { + Self(Vec::new()) + } + /// Create a [`CallArguments`] from an iterator over non-variadic positional argument types. pub(crate) fn positional(positional_tys: impl IntoIterator>) -> Self { positional_tys @@ -29,6 +34,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/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 44c0d298c2..e8cc8c0d26 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -193,6 +193,13 @@ impl<'db> CallBinding<'db> { } } + pub(crate) fn three_parameter_types(&self) -> Option<(Type<'db>, Type<'db>, Type<'db>)> { + match self.parameter_types() { + [first, second, third] => Some((*first, *second, *third)), + _ => None, + } + } + fn callable_name(&self, db: &'db dyn Db) -> Option<&str> { match self.callable_ty { Type::FunctionLiteral(function) => Some(function.name(db)), diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 1dd6881b10..e3ac7bc7d1 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -72,6 +72,7 @@ impl<'db> ClassBase<'db> { Type::Never | Type::BooleanLiteral(_) | Type::FunctionLiteral(_) + | Type::Callable(..) | Type::BytesLiteral(_) | Type::IntLiteral(_) | Type::StringLiteral(_) diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 23b6395ca4..95dd427aa6 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -8,8 +8,8 @@ use ruff_python_literal::escape::AsciiEscape; use crate::types::class_base::ClassBase; use crate::types::{ - ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType, Type, - UnionType, + CallableType, ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType, + Type, UnionType, }; use crate::Db; use rustc_hash::FxHashMap; @@ -88,6 +88,24 @@ impl Display for DisplayRepresentation<'_> { }, Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)), Type::FunctionLiteral(function) => f.write_str(function.name(self.db)), + Type::Callable(CallableType::BoundMethod(bound_method)) => { + write!( + f, + "", + method = bound_method.function(self.db).name(self.db), + instance = bound_method.self_instance(self.db).display(self.db) + ) + } + Type::Callable(CallableType::MethodWrapperDunderGet(function)) => { + write!( + f, + "", + function = function.name(self.db) + ) + } + Type::Callable(CallableType::WrapperDescriptorDunderGet) => { + f.write_str("") + } Type::Union(union) => union.display(self.db).fmt(f), Type::Intersection(intersection) => intersection.display(self.db).fmt(f), Type::IntLiteral(n) => n.fmt(f), diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 02b269bb2a..468dc27c66 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -3753,6 +3753,7 @@ impl<'db> TypeInferenceBuilder<'db> { ( op @ (ast::UnaryOp::UAdd | ast::UnaryOp::USub | ast::UnaryOp::Invert), Type::FunctionLiteral(_) + | Type::Callable(..) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) | Type::SubclassOf(_) @@ -3780,7 +3781,7 @@ impl<'db> TypeInferenceBuilder<'db> { match operand_type.try_call_dunder( self.db(), unary_dunder_method, - &CallArguments::positional([operand_type]), + &CallArguments::none(), ) { Ok(outcome) => outcome.return_type(self.db()), Err(e) => { @@ -3967,6 +3968,7 @@ impl<'db> TypeInferenceBuilder<'db> { // fall back on looking for dunder methods on one of the operand types. ( Type::FunctionLiteral(_) + | Type::Callable(..) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) | Type::SubclassOf(_) @@ -3983,6 +3985,7 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::SliceLiteral(_) | Type::Tuple(_), Type::FunctionLiteral(_) + | Type::Callable(..) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) | Type::SubclassOf(_) @@ -4029,7 +4032,7 @@ impl<'db> TypeInferenceBuilder<'db> { .try_call_dunder( self.db(), reflected_dunder, - &CallArguments::positional([right_ty, left_ty]), + &CallArguments::positional([left_ty]), ) .map(|outcome| outcome.return_type(self.db())) .or_else(|_| { @@ -4037,7 +4040,7 @@ impl<'db> TypeInferenceBuilder<'db> { .try_call_dunder( self.db(), op.dunder(), - &CallArguments::positional([left_ty, right_ty]), + &CallArguments::positional([right_ty]), ) .map(|outcome| outcome.return_type(self.db())) }) @@ -4953,7 +4956,7 @@ impl<'db> TypeInferenceBuilder<'db> { match value_ty.try_call_dunder( self.db(), "__getitem__", - &CallArguments::positional([value_ty, slice_ty]), + &CallArguments::positional([slice_ty]), ) { Ok(outcome) => return outcome.return_type(self.db()), Err(err @ CallDunderError::PossiblyUnbound { .. }) => { diff --git a/crates/red_knot_python_semantic/src/types/property_tests.rs b/crates/red_knot_python_semantic/src/types/property_tests.rs index c1606443f4..834e341767 100644 --- a/crates/red_knot_python_semantic/src/types/property_tests.rs +++ b/crates/red_knot_python_semantic/src/types/property_tests.rs @@ -29,9 +29,10 @@ use std::sync::{Arc, Mutex, MutexGuard, OnceLock}; use crate::db::tests::{setup_db, TestDb}; use crate::symbol::{builtins_symbol, known_module_symbol}; use crate::types::{ - IntersectionBuilder, KnownClass, KnownInstanceType, SubclassOfType, TupleType, Type, UnionType, + BoundMethodType, CallableType, IntersectionBuilder, KnownClass, KnownInstanceType, + SubclassOfType, TupleType, Type, UnionType, }; -use crate::KnownModule; +use crate::{Db, KnownModule}; use quickcheck::{Arbitrary, Gen}; /// A test representation of a type that can be transformed unambiguously into a real Type, @@ -67,6 +68,24 @@ pub(crate) enum Ty { SubclassOfAbcClass(&'static str), AlwaysTruthy, AlwaysFalsy, + BuiltinsFunction(&'static str), + BuiltinsBoundMethod { + class: &'static str, + method: &'static str, + }, +} + +#[salsa::tracked] +fn create_bound_method<'db>( + db: &'db dyn Db, + function: Type<'db>, + builtins_class: Type<'db>, +) -> Type<'db> { + Type::Callable(CallableType::BoundMethod(BoundMethodType::new( + db, + function.expect_function_literal(), + builtins_class.to_instance(db), + ))) } impl Ty { @@ -123,6 +142,13 @@ impl Ty { ), Ty::AlwaysTruthy => Type::AlwaysTruthy, Ty::AlwaysFalsy => Type::AlwaysFalsy, + Ty::BuiltinsFunction(name) => builtins_symbol(db, name).expect_type(), + Ty::BuiltinsBoundMethod { class, method } => { + let builtins_class = builtins_symbol(db, class).expect_type(); + let function = builtins_class.static_member(db, method).expect_type(); + + create_bound_method(db, function, builtins_class) + } } } } @@ -173,6 +199,16 @@ fn arbitrary_core_type(g: &mut Gen) -> Ty { Ty::SubclassOfAbcClass("ABCMeta"), Ty::AlwaysTruthy, Ty::AlwaysFalsy, + Ty::BuiltinsFunction("chr"), + Ty::BuiltinsFunction("ascii"), + Ty::BuiltinsBoundMethod { + class: "str", + method: "isascii", + }, + Ty::BuiltinsBoundMethod { + class: "int", + method: "bit_length", + }, ]) .unwrap() .clone() 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>, diff --git a/crates/red_knot_python_semantic/src/types/subclass_of.rs b/crates/red_knot_python_semantic/src/types/subclass_of.rs index 4a0705dfcf..8820993d07 100644 --- a/crates/red_knot_python_semantic/src/types/subclass_of.rs +++ b/crates/red_knot_python_semantic/src/types/subclass_of.rs @@ -64,8 +64,8 @@ impl<'db> SubclassOfType<'db> { !self.is_dynamic() } - pub(crate) fn member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { - Type::from(self.subclass_of).member(db, name) + pub(crate) fn static_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { + Type::from(self.subclass_of).static_member(db, name) } /// Return `true` if `self` is a subtype of `other`. diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index 1b6e63a764..483843feb0 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -1,5 +1,7 @@ use std::cmp::Ordering; +use crate::types::CallableType; + use super::{ class_base::ClassBase, ClassLiteralType, DynamicType, InstanceType, KnownInstanceType, TodoType, Type, @@ -54,6 +56,27 @@ pub(super) fn union_elements_ordering<'db>(left: &Type<'db>, right: &Type<'db>) (Type::FunctionLiteral(_), _) => Ordering::Less, (_, Type::FunctionLiteral(_)) => Ordering::Greater, + ( + Type::Callable(CallableType::BoundMethod(left)), + Type::Callable(CallableType::BoundMethod(right)), + ) => left.cmp(right), + (Type::Callable(CallableType::BoundMethod(_)), _) => Ordering::Less, + (_, Type::Callable(CallableType::BoundMethod(_))) => Ordering::Greater, + + ( + Type::Callable(CallableType::MethodWrapperDunderGet(left)), + Type::Callable(CallableType::MethodWrapperDunderGet(right)), + ) => left.cmp(right), + (Type::Callable(CallableType::MethodWrapperDunderGet(_)), _) => Ordering::Less, + (_, Type::Callable(CallableType::MethodWrapperDunderGet(_))) => Ordering::Greater, + + ( + Type::Callable(CallableType::WrapperDescriptorDunderGet), + Type::Callable(CallableType::WrapperDescriptorDunderGet), + ) => Ordering::Equal, + (Type::Callable(CallableType::WrapperDescriptorDunderGet), _) => Ordering::Less, + (_, Type::Callable(CallableType::WrapperDescriptorDunderGet)) => Ordering::Greater, + (Type::Tuple(left), Type::Tuple(right)) => left.cmp(right), (Type::Tuple(_), _) => Ordering::Less, (_, Type::Tuple(_)) => Ordering::Greater,