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 c4c241865d..87cf536333 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(return type of decorated function) +reveal_type(foo.join(qux)) # revealed: @Todo(return type of overloaded function) template: LiteralString = "{}, {}" reveal_type(template) # revealed: Literal["{}, {}"] # TODO: Infer `LiteralString` -reveal_type(template.format(foo, bar)) # revealed: @Todo(return type of decorated function) +reveal_type(template.format(foo, bar)) # revealed: @Todo(return type of overloaded function) ``` ### Assignability diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md index b62aff8408..9bdac93b90 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -1541,7 +1541,7 @@ integers are instances of that class: ```py reveal_type((2).bit_length) # revealed: -reveal_type((2).denominator) # revealed: @Todo(@property) +reveal_type((2).denominator) # revealed: Literal[1] ``` Some attributes are special-cased, however: diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md index a8354eed19..b435a34d76 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/instances.md @@ -312,7 +312,7 @@ reveal_type(1 + A()) # revealed: A reveal_type(A() + "foo") # revealed: A # TODO should be `A` since `str.__add__` doesn't support `A` instances # TODO overloads -reveal_type("foo" + A()) # revealed: @Todo(return type of decorated function) +reveal_type("foo" + A()) # revealed: @Todo(return type of overloaded function) reveal_type(A() + b"foo") # revealed: A # TODO should be `A` since `bytes.__add__` doesn't support `A` instances @@ -320,7 +320,7 @@ reveal_type(b"foo" + A()) # revealed: bytes reveal_type(A() + ()) # revealed: A # TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances -reveal_type(() + A()) # revealed: @Todo(return type of decorated function) +reveal_type(() + A()) # revealed: @Todo(return type of overloaded function) literal_string_instance = "foo" * 1_000_000_000 # the test is not testing what it's meant to be testing if this isn't a `LiteralString`: @@ -329,7 +329,7 @@ reveal_type(literal_string_instance) # revealed: LiteralString reveal_type(A() + literal_string_instance) # revealed: A # TODO should be `A` since `str.__add__` doesn't support `A` instances # TODO overloads -reveal_type(literal_string_instance + A()) # revealed: @Todo(return type of decorated function) +reveal_type(literal_string_instance + A()) # revealed: @Todo(return type of overloaded function) ``` ## Operations involving instances of classes inheriting from `Any` diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md b/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md index ffc8081165..a041bbf47a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md @@ -50,9 +50,9 @@ reveal_type(1 ** (largest_u32 + 1)) # revealed: int reveal_type(2**largest_u32) # revealed: int def variable(x: int): - reveal_type(x**2) # revealed: @Todo(return type of decorated function) - reveal_type(2**x) # revealed: @Todo(return type of decorated function) - reveal_type(x**x) # revealed: @Todo(return type of decorated function) + reveal_type(x**2) # revealed: @Todo(return type of overloaded function) + reveal_type(2**x) # revealed: @Todo(return type of overloaded function) + reveal_type(x**x) # revealed: @Todo(return type of overloaded function) ``` ## Division by Zero diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/function.md b/crates/red_knot_python_semantic/resources/mdtest/call/function.md index e0cd44e5d2..b4ff45cc61 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/function.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/function.md @@ -43,8 +43,7 @@ def decorator(func) -> Callable[[], int]: def bar() -> str: return "bar" -# TODO: should reveal `int`, as the decorator replaces `bar` with `foo` -reveal_type(bar()) # revealed: @Todo(return type of decorated function) +reveal_type(bar()) # revealed: int ``` ## Invalid callable 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 index 19d012e615..f781de4398 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/getattr_static.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/getattr_static.md @@ -59,7 +59,7 @@ 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[real] +reveal_type(inspect.getattr_static(1, "real")) # revealed: property ``` (Implicit) instance attributes can also be accessed through `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 index 624fdbfc58..5aa3f0e3d2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md @@ -410,23 +410,29 @@ def does_nothing[T](f: T) -> T: class C: @classmethod + # TODO: no error should be emitted here (needs support for generics) + # error: [invalid-argument-type] @does_nothing def f1(cls: type[C], x: int) -> str: return "a" - + # TODO: no error should be emitted here (needs support for generics) + # error: [invalid-argument-type] @does_nothing @classmethod def f2(cls: type[C], x: int) -> str: return "a" -# TODO: We do not support decorators yet (only limited special cases). Eventually, -# these should all return `str`: +# TODO: All of these should be `str` (and not emit an error), once we support generics -reveal_type(C.f1(1)) # revealed: @Todo(return type of decorated function) -reveal_type(C().f1(1)) # revealed: @Todo(return type of decorated function) +# error: [call-non-callable] +reveal_type(C.f1(1)) # revealed: Unknown +# error: [call-non-callable] +reveal_type(C().f1(1)) # revealed: Unknown -reveal_type(C.f2(1)) # revealed: @Todo(return type of decorated function) -reveal_type(C().f2(1)) # revealed: @Todo(return type of decorated function) +# error: [call-non-callable] +reveal_type(C.f2(1)) # revealed: Unknown +# error: [call-non-callable] +reveal_type(C().f2(1)) # revealed: Unknown ``` [functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods diff --git a/crates/red_knot_python_semantic/resources/mdtest/decorators.md b/crates/red_knot_python_semantic/resources/mdtest/decorators.md new file mode 100644 index 0000000000..c13002fcc5 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/decorators.md @@ -0,0 +1,237 @@ +# Decorators + +Decorators are a way to modify function and class behavior. A decorator is a callable that takes the +function or class as an argument and returns a modified version of it. + +## Basic example + +A decorated function definition is conceptually similar to `def f(x): ...` followed by +`f = decorator(f)`. This means that the type of a decorated function is the same as the return type +of the decorator (which does not necessarily need to be a callable type): + +```py +def custom_decorator(f) -> int: + return 1 + +@custom_decorator +def f(x): ... + +reveal_type(f) # revealed: int +``` + +## Type-annotated decorator + +More commonly, a decorator returns a modified callable type: + +```py +from typing import Callable + +def ensure_positive(wrapped: Callable[[int], bool]) -> Callable[[int], bool]: + return lambda x: wrapped(x) and x > 0 + +@ensure_positive +def even(x: int) -> bool: + return x % 2 == 0 + +reveal_type(even) # revealed: (int, /) -> bool +reveal_type(even(4)) # revealed: bool +``` + +## Decorators which take arguments + +Decorators can be arbitrary expressions. This is often useful when the decorator itself takes +arguments: + +```py +from typing import Callable + +def ensure_larger_than(lower_bound: int) -> Callable[[Callable[[int], bool]], Callable[[int], bool]]: + def decorator(wrapped: Callable[[int], bool]) -> Callable[[int], bool]: + return lambda x: wrapped(x) and x >= lower_bound + return decorator + +@ensure_larger_than(10) +def even(x: int) -> bool: + return x % 2 == 0 + +reveal_type(even) # revealed: (int, /) -> bool +reveal_type(even(14)) # revealed: bool +``` + +## Multiple decorators + +Multiple decorators can be applied to a single function. They are applied in "bottom-up" order, +meaning that the decorator closest to the function definition is applied first: + +```py +def maps_to_str(f) -> str: + return "a" + +def maps_to_int(f) -> int: + return 1 + +def maps_to_bytes(f) -> bytes: + return b"a" + +@maps_to_str +@maps_to_int +@maps_to_bytes +def f(x): ... + +reveal_type(f) # revealed: str +``` + +## Decorating with a class + +When a function is decorated with a class-based decorator, the decorated function turns into an +instance of the class (see also: [properties](properties.md)). Attributes of the class can be +accessed on the decorated function. + +```py +class accept_strings: + custom_attribute: str = "a" + + def __init__(self, f): + self.f = f + + def __call__(self, x: str | int) -> bool: + return self.f(int(x)) + +@accept_strings +def even(x: int) -> bool: + return x > 0 + +reveal_type(even) # revealed: accept_strings +reveal_type(even.custom_attribute) # revealed: str +reveal_type(even("1")) # revealed: bool +reveal_type(even(1)) # revealed: bool + +# error: [invalid-argument-type] +even(None) +``` + +## Common decorator patterns + +### `functools.wraps` + +This test mainly makes sure that we do not emit any diagnostics in a case where the decorator is +implemented using `functools.wraps`. + +```py +from typing import Callable +from functools import wraps + +def custom_decorator(f) -> Callable[[int], str]: + @wraps(f) + def wrapper(*args, **kwargs): + print("Calling decorated function") + return f(*args, **kwargs) + return wrapper + +@custom_decorator +def f(x: int) -> str: + return str(x) + +reveal_type(f) # revealed: (int, /) -> str +``` + +### `functools.cache` + +```py +from functools import cache + +@cache +def f(x: int) -> int: + return x**2 + +# TODO: Should be `_lru_cache_wrapper[int]` +reveal_type(f) # revealed: @Todo(generics) + +# TODO: Should be `int` +reveal_type(f(1)) # revealed: @Todo(generics) +``` + +## Lambdas as decorators + +```py +@lambda f: f +def g(x: int) -> str: + return "a" + +# TODO: This should be `Literal[g]` or `(int, /) -> str` +reveal_type(g) # revealed: Unknown +``` + +## Error cases + +### Unknown decorator + +```py +# error: [unresolved-reference] "Name `unknown_decorator` used when not defined" +@unknown_decorator +def f(x): ... + +reveal_type(f) # revealed: Unknown +``` + +### Error in the decorator expression + +```py +# error: [unsupported-operator] +@(1 + "a") +def f(x): ... + +reveal_type(f) # revealed: Unknown +``` + +### Non-callable decorator + +```py +non_callable = 1 + +# error: [call-non-callable] "Object of type `Literal[1]` is not callable" +@non_callable +def f(x): ... + +reveal_type(f) # revealed: Unknown +``` + +### Wrong signature + +#### Wrong argument type + +Here, we emit a diagnostic since `wrong_signature` takes an `int` instead of a callable type as the +first argument: + +```py +def wrong_signature(f: int) -> str: + return "a" + +# error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 1 (`f`) of function `wrong_signature`; expected type `int`" +@wrong_signature +def f(x): ... + +reveal_type(f) # revealed: str +``` + +#### Wrong number of arguments + +Decorators need to be callable with a single argument. If they are not, we emit a diagnostic: + +```py +def takes_two_arguments(f, g) -> str: + return "a" + +# error: [missing-argument] "No argument provided for required parameter `g` of function `takes_two_arguments`" +@takes_two_arguments +def f(x): ... + +reveal_type(f) # revealed: str + +def takes_no_argument() -> str: + return "a" + +# error: [too-many-positional-arguments] "Too many positional arguments to function `takes_no_argument`: expected 0, got 1" +@takes_no_argument +def g(x): ... +``` 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 528d6965a2..f23409a62f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md @@ -506,8 +506,7 @@ class C: @property def name(self) -> str: return self._name or "Unset" - # TODO: No diagnostic should be emitted here - # error: [unresolved-attribute] "Type `Literal[name]` has no attribute `setter`" + @name.setter def name(self, value: str | None) -> None: self._value = value @@ -515,22 +514,13 @@ class C: c = C() reveal_type(c._name) # revealed: str | None +reveal_type(c.name) # revealed: str +reveal_type(C.name) # revealed: property -# TODO: Should be `str` -reveal_type(c.name) # revealed: - -# Should be `builtins.property` -reveal_type(C.name) # revealed: Literal[name] - -# TODO: These should not emit errors -# error: [invalid-assignment] c.name = "new" - -# error: [invalid-assignment] c.name = None -# TODO: this should be an error, but with a proper error message -# error: [invalid-assignment] "Implicit shadowing of function `name`; annotate to make it explicit if this is intentional" +# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `name` on type `C` with custom `__set__` method" c.name = 42 ``` @@ -587,7 +577,7 @@ reveal_type(wrapper_descriptor(f, None, type(f))) # revealed: Literal[f] reveal_type(f.__get__.__hash__) # revealed: # Attribute access on the wrapper-descriptor falls back to `WrapperDescriptorType`: -reveal_type(wrapper_descriptor.__qualname__) # revealed: @Todo(@property) +reveal_type(wrapper_descriptor.__qualname__) # revealed: str ``` We can also bind the free function `f` to an instance of a class `C`: diff --git a/crates/red_knot_python_semantic/resources/mdtest/pep695_type_aliases.md b/crates/red_knot_python_semantic/resources/mdtest/pep695_type_aliases.md index 4763ec307d..43a0cf6c6a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/pep695_type_aliases.md +++ b/crates/red_knot_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -28,10 +28,7 @@ def f() -> None: ```py type IntOrStr = int | str -# TODO: This should either fall back to the specified type from typeshed, -# which is `Any`, or be the actual type of the runtime value expression -# `int | str`, i.e. `types.UnionType`. -reveal_type(IntOrStr.__value__) # revealed: @Todo(@property) +reveal_type(IntOrStr.__value__) # revealed: Any ``` ## Invalid assignment @@ -74,7 +71,7 @@ type ListOrSet[T] = list[T] | set[T] # TODO: Should be `tuple[typing.TypeVar | typing.ParamSpec | typing.TypeVarTuple, ...]`, # as specified in the `typeshed` stubs. -reveal_type(ListOrSet.__type_params__) # revealed: @Todo(@property) +reveal_type(ListOrSet.__type_params__) # revealed: @Todo(full tuple[...] support) ``` ## `TypeAliasType` properties diff --git a/crates/red_knot_python_semantic/resources/mdtest/properties.md b/crates/red_knot_python_semantic/resources/mdtest/properties.md new file mode 100644 index 0000000000..de16979c66 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/properties.md @@ -0,0 +1,305 @@ +# Properties + +`property` is a built-in class in Python that can be used to model class attributes with custom +getters, setters, and deleters. + +## Basic getter + +`property` is typically used as a decorator on a getter method. It turns the method into a property +object. When accessing the property on an instance, the descriptor protocol is invoked, which calls +the getter method: + +```py +class C: + @property + def my_property(self) -> int: + return 1 + +reveal_type(C().my_property) # revealed: int +``` + +When a property is accessed on the class directly, the descriptor protocol is also invoked, but +`property.__get__` simply returns itself in this case (when `instance` is `None`): + +```py +reveal_type(C.my_property) # revealed: property +``` + +## Getter and setter + +A property can also have a setter method, which is used to set the value of the property. The setter +method is defined using the `@.setter` decorator. The setter method takes the value +to be set as an argument. + +```py +class C: + @property + def my_property(self) -> int: + return 1 + + @my_property.setter + def my_property(self, value: int) -> None: + pass + +c = C() +reveal_type(c.my_property) # revealed: int +c.my_property = 2 + +# error: [invalid-assignment] +c.my_property = "a" +``` + +## `property.getter` + +`property.getter` can be used to overwrite the getter method of a property. This does not overwrite +the existing setter: + +```py +class C: + @property + def my_property(self) -> int: + return 1 + + @my_property.setter + def my_property(self, value: int) -> None: + pass + + @my_property.getter + def my_property(self) -> str: + return "a" + +c = C() +reveal_type(c.my_property) # revealed: str +c.my_property = 2 + +# error: [invalid-assignment] +c.my_property = "b" +``` + +## `property.deleter` + +We do not support `property.deleter` yet, but we make sure that it does not invalidate the getter or +setter: + +```py +class C: + @property + def my_property(self) -> int: + return 1 + + @my_property.setter + def my_property(self, value: int) -> None: + pass + + @my_property.deleter + def my_property(self) -> None: + pass + +c = C() +reveal_type(c.my_property) # revealed: int +c.my_property = 2 +# error: [invalid-assignment] +c.my_property = "a" +``` + +## Failure cases + +### Attempting to write to a read-only property + +When attempting to write to a read-only property, we emit an error: + +```py +class C: + @property + def attr(self) -> int: + return 1 + +c = C() + +# error: [invalid-assignment] +c.attr = 2 +``` + +### Attempting to read a write-only property + +When attempting to read a write-only property, we emit an error: + +```py +class C: + def attr_setter(self, value: int) -> None: + pass + attr = property(fset=attr_setter) + +c = C() +c.attr = 1 + +# TODO: An error should be emitted here, and the type should be `Unknown` +# or `Never`. See https://github.com/astral-sh/ruff/issues/16298 for more +# details. +reveal_type(c.attr) # revealed: Unknown | property +``` + +### Wrong setter signature + +```py +class C: + @property + def attr(self) -> int: + return 1 + # error: [invalid-argument-type] "Object of type `Literal[attr]` cannot be assigned to parameter 2 (`fset`) of bound method `setter`; expected type `(Any, Any, /) -> None`" + @attr.setter + def attr(self) -> None: + pass +``` + +### Wrong getter signature + +```py +class C: + # error: [invalid-argument-type] "Object of type `Literal[attr]` cannot be assigned to parameter 1 (`fget`) of class `property`; expected type `((Any, /) -> Any) | None`" + @property + def attr(self, x: int) -> int: + return 1 +``` + +## Limitations + +### Manually constructed property + +Properties can also be constructed manually using the `property` class. We partially support this: + +```py +class C: + def attr_getter(self) -> int: + return 1 + attr = property(attr_getter) + +c = C() +reveal_type(c.attr) # revealed: Unknown | int +``` + +But note that we return `Unknown | int` because we did not declare the `attr` attribute. This is +consistent with how we usually treat attributes, but here, if we try to declare `attr` as +`property`, we fail to understand the property, since the `property` declaration shadows the more +precise type that we infer for `property(attr_getter)` (which includes the actual information about +the getter). + +```py +class C: + def attr_getter(self) -> int: + return 1 + attr: property = property(attr_getter) + +c = C() +reveal_type(c.attr) # revealed: Unknown +``` + +## Behind the scenes + +In this section, we trace through some of the steps that make properties work. We start with a +simple class `C` and a property `attr`: + +```py +class C: + def __init__(self): + self._attr: int = 0 + + @property + def attr(self) -> int: + return self._attr + + @attr.setter + def attr(self, value: str) -> None: + self._attr = len(value) +``` + +Next, we create an instance of `C`. As we have seen above, accessing `attr` on the instance will +return an `int`: + +```py +c = C() + +reveal_type(c.attr) # revealed: int +``` + +Behind the scenes, when we write `c.attr`, the first thing that happens is that we statically look +up the symbol `attr` on the meta-type of `c`, i.e. the class `C`. We can emulate this static lookup +using `inspect.getattr_static`, to see that `attr` is actually an instance of the `property` class: + +```py +from inspect import getattr_static + +attr_property = getattr_static(C, "attr") +reveal_type(attr_property) # revealed: property +``` + +The `property` class has a `__get__` method, which makes it a descriptor. It also has a `__set__` +method, which means that it is a *data* descriptor (if there is no setter, `__set__` is still +available but yields an `AttributeError` at runtime). + +```py +reveal_type(type(attr_property).__get__) # revealed: +reveal_type(type(attr_property).__set__) # revealed: +``` + +When we access `c.attr`, the `__get__` method of the `property` class is called, passing the +property object itself as the first argument, and the class instance `c` as the second argument. The +third argument is the "owner" which can be set to `None` or to `C` in this case: + +```py +reveal_type(type(attr_property).__get__(attr_property, c, C)) # revealed: int +reveal_type(type(attr_property).__get__(attr_property, c, None)) # revealed: int +``` + +Alternatively, the above can also be written as a method call: + +```py +reveal_type(attr_property.__get__(c, C)) # revealed: int +``` + +When we access `attr` on the class itself, the descriptor protocol is also invoked, but the instance +argument is set to `None`. When `instance` is `None`, the call to `property.__get__` returns the +property instance itself. So the following expressions are all equivalent + +```py +reveal_type(attr_property) # revealed: property +reveal_type(C.attr) # revealed: property +reveal_type(attr_property.__get__(None, C)) # revealed: property +reveal_type(type(attr_property).__get__(attr_property, None, C)) # revealed: property +``` + +When we set the property using `c.attr = "a"`, the `__set__` method of the property class is called. +This attribute access desugars to + +```py +type(attr_property).__set__(attr_property, c, "a") + +# error: [call-non-callable] "Call of wrapper descriptor `property.__set__` failed: calling the setter failed" +type(attr_property).__set__(attr_property, c, 1) +``` + +which is also equivalent to the following expressions: + +```py +attr_property.__set__(c, "a") +# error: [call-non-callable] +attr_property.__set__(c, 1) + +C.attr.__set__(c, "a") +# error: [call-non-callable] +C.attr.__set__(c, 1) +``` + +Properties also have `fget` and `fset` attributes that can be used to retrieve the original getter +and setter functions, respectively. + +```py +reveal_type(attr_property.fget) # revealed: Literal[attr] +reveal_type(attr_property.fget(c)) # revealed: int + +reveal_type(attr_property.fset) # revealed: Literal[attr] +reveal_type(attr_property.fset(c, "a")) # revealed: None + +# error: [invalid-argument-type] +attr_property.fset(c, 1) +``` 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 7b5f55ffdf..aa6d47cd87 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 @@ -58,9 +58,8 @@ reveal_type(typing.__eq__) # revealed: reveal_type(typing.__class__) # revealed: Literal[ModuleType] -# TODO: needs support for attribute access on instances, properties and generics; -# should be `dict[str, Any]` -reveal_type(typing.__dict__) # revealed: @Todo(@property) +# TODO: needs support generics; should be `dict[str, Any]`: +reveal_type(typing.__dict__) # revealed: @Todo(generics) ``` Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType` to help out with @@ -92,10 +91,9 @@ reveal_type(__dict__) # revealed: Literal["foo"] import foo from foo import __dict__ as foo_dict -# TODO: needs support for attribute access on instances, properties, and generics; -# should be `dict[str, Any]` for both of these: -reveal_type(foo.__dict__) # revealed: @Todo(@property) -reveal_type(foo_dict) # revealed: @Todo(@property) +# TODO: needs support generics; should be `dict[str, Any]` for both of these: +reveal_type(foo.__dict__) # revealed: @Todo(generics) +reveal_type(foo_dict) # revealed: @Todo(generics) ``` ## Conditionally global or `ModuleType` attribute diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md index 7371c10562..255cb6bc9d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md @@ -25,7 +25,7 @@ reveal_type(y) # revealed: Unknown def _(n: int): a = b"abcde"[n] # TODO: Support overloads... Should be `bytes` - reveal_type(a) # revealed: @Todo(return type of decorated function) + reveal_type(a) # revealed: @Todo(return type of overloaded function) ``` ## Slices @@ -44,10 +44,10 @@ b[::0] # error: [zero-stepsize-in-slice] def _(m: int, n: int): byte_slice1 = b[m:n] # TODO: Support overloads... Should be `bytes` - reveal_type(byte_slice1) # revealed: @Todo(return type of decorated function) + reveal_type(byte_slice1) # revealed: @Todo(return type of overloaded function) def _(s: bytes) -> bytes: byte_slice2 = s[0:5] # TODO: Support overloads... Should be `bytes` - return reveal_type(byte_slice2) # revealed: @Todo(return type of decorated function) + return reveal_type(byte_slice2) # revealed: @Todo(return type of overloaded function) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md index 9c1c727c07..408a4f43b6 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/lists.md @@ -12,13 +12,13 @@ x = [1, 2, 3] reveal_type(x) # revealed: list # TODO reveal int -reveal_type(x[0]) # revealed: @Todo(return type of decorated function) +reveal_type(x[0]) # revealed: @Todo(return type of overloaded function) # TODO reveal list -reveal_type(x[0:1]) # revealed: @Todo(return type of decorated function) +reveal_type(x[0:1]) # revealed: @Todo(return type of overloaded function) # TODO error -reveal_type(x["a"]) # revealed: @Todo(return type of decorated function) +reveal_type(x["a"]) # revealed: @Todo(return type of overloaded function) ``` ## Assignments within list assignment diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md index 240804de88..9a875dc323 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md @@ -22,7 +22,7 @@ reveal_type(b) # revealed: Unknown def _(n: int): a = "abcde"[n] # TODO: Support overloads... Should be `str` - reveal_type(a) # revealed: @Todo(return type of decorated function) + reveal_type(a) # revealed: @Todo(return type of overloaded function) ``` ## Slices @@ -76,11 +76,11 @@ def _(m: int, n: int, s2: str): substring1 = s[m:n] # TODO: Support overloads... Should be `LiteralString` - reveal_type(substring1) # revealed: @Todo(return type of decorated function) + reveal_type(substring1) # revealed: @Todo(return type of overloaded function) substring2 = s2[0:5] # TODO: Support overloads... Should be `str` - reveal_type(substring2) # revealed: @Todo(return type of decorated function) + reveal_type(substring2) # revealed: @Todo(return type of overloaded function) ``` ## Unsupported slice types diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md index fe2b03ef74..8ac7ff2534 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md @@ -70,7 +70,7 @@ def _(m: int, n: int): tuple_slice = t[m:n] # TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]` - reveal_type(tuple_slice) # revealed: @Todo(return type of decorated function) + reveal_type(tuple_slice) # revealed: @Todo(return type of overloaded function) ``` ## Inheritance diff --git a/crates/red_knot_python_semantic/resources/mdtest/suppressions/no_type_check.md b/crates/red_knot_python_semantic/resources/mdtest/suppressions/no_type_check.md index 82327cdbe6..b9fd685c45 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/suppressions/no_type_check.md +++ b/crates/red_knot_python_semantic/resources/mdtest/suppressions/no_type_check.md @@ -48,6 +48,8 @@ from typing import no_type_check @unknown_decorator # error: [unresolved-reference] @no_type_check def test() -> int: + # TODO: this should not be an error + # error: [unresolved-reference] return a + 5 ``` @@ -64,6 +66,8 @@ from typing import no_type_check @no_type_check @unknown_decorator def test() -> int: + # TODO: this should not be an error + # error: [unresolved-reference] return a + 5 ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/sys_version_info.md b/crates/red_knot_python_semantic/resources/mdtest/sys_version_info.md index d0144b7c1e..bedcdb20b9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/sys_version_info.md +++ b/crates/red_knot_python_semantic/resources/mdtest/sys_version_info.md @@ -121,9 +121,9 @@ But the `micro`, `releaselevel` and `serial` fields are inferred as `@Todo` unti properties on instance types: ```py -reveal_type(sys.version_info.micro) # revealed: @Todo(@property) -reveal_type(sys.version_info.releaselevel) # revealed: @Todo(@property) -reveal_type(sys.version_info.serial) # revealed: @Todo(@property) +reveal_type(sys.version_info.micro) # revealed: int +reveal_type(sys.version_info.releaselevel) # revealed: @Todo(Support for `typing.TypeAlias`) +reveal_type(sys.version_info.serial) # revealed: int ``` ## Accessing fields by index/slice diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 1a26ff4834..308cd66ee3 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -227,6 +227,13 @@ macro_rules! todo_type { pub(crate) use todo_type; +/// Represents an instance of `builtins.property`. +#[salsa::interned(debug)] +pub struct PropertyInstanceType<'db> { + getter: Option>, + setter: Option>, +} + /// Representation of a type: a set of possible values at runtime. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update)] pub enum Type<'db> { @@ -247,7 +254,7 @@ pub enum Type<'db> { /// the `self` parameter, and return a `MethodType & 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. + /// Represents a specific instance of `types.MethodWrapperType`. /// /// TODO: consider replacing this with `Callable & types.MethodWrapperType` type? /// Requires `Callable` to be able to represent overloads, e.g. `types.FunctionType.__get__` has @@ -257,13 +264,13 @@ pub enum Type<'db> { /// * (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__`. + MethodWrapper(MethodWrapperKind<'db>), + /// Represents a specific instance of `types.WrapperDescriptorType`. /// /// 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, + WrapperDescriptor(WrapperDescriptorKind), /// The type of an arbitrary callable object with a certain specified signature. Callable(CallableType<'db>), /// A specific module object @@ -276,6 +283,8 @@ pub enum Type<'db> { Instance(InstanceType<'db>), /// A single Python object that requires special treatment in the type system KnownInstance(KnownInstanceType<'db>), + /// An instance of `builtins.property` + PropertyInstance(PropertyInstanceType<'db>), /// The set of objects in any of the types in the union Union(UnionType<'db>), /// The set of objects in all of the types in the intersection @@ -362,14 +371,15 @@ impl<'db> Type<'db> { | Self::ModuleLiteral(_) | Self::ClassLiteral(_) | Self::KnownInstance(_) + | Self::PropertyInstance(_) | Self::StringLiteral(_) | Self::IntLiteral(_) | Self::LiteralString | Self::SliceLiteral(_) | Self::Dynamic(DynamicType::Unknown | DynamicType::Any) | Self::BoundMethod(_) - | Self::WrapperDescriptorDunderGet - | Self::MethodWrapperDunderGet(_) => false, + | Self::WrapperDescriptor(_) + | Self::MethodWrapper(_) => false, Self::Callable(callable) => { let signature = callable.signature(db); @@ -428,6 +438,10 @@ impl<'db> Type<'db> { matches!(self, Type::Instance(..)) } + pub const fn is_property_instance(&self) -> bool { + matches!(self, Type::PropertyInstance(..)) + } + pub fn module_literal(db: &'db dyn Db, importing_file: File, submodule: Module) -> Self { Self::ModuleLiteral(ModuleLiteralType::new(db, importing_file, submodule)) } @@ -596,6 +610,7 @@ impl<'db> Type<'db> { } Type::LiteralString | Type::Instance(_) + | Type::PropertyInstance(_) | Type::AlwaysFalsy | Type::AlwaysTruthy | Type::BooleanLiteral(_) @@ -605,9 +620,9 @@ impl<'db> Type<'db> { | Type::Dynamic(_) | Type::Never | Type::FunctionLiteral(_) - | Type::MethodWrapperDunderGet(_) + | Type::MethodWrapper(_) | Type::BoundMethod(_) - | Type::WrapperDescriptorDunderGet + | Type::WrapperDescriptor(_) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) | Type::KnownInstance(_) @@ -741,10 +756,10 @@ impl<'db> Type<'db> { (Type::BoundMethod(_), _) => KnownClass::MethodType .to_instance(db) .is_subtype_of(db, target), - (Type::MethodWrapperDunderGet(_), _) => KnownClass::WrapperDescriptorType + (Type::MethodWrapper(_), _) => KnownClass::WrapperDescriptorType .to_instance(db) .is_subtype_of(db, target), - (Type::WrapperDescriptorDunderGet, _) => KnownClass::WrapperDescriptorType + (Type::WrapperDescriptor(_), _) => KnownClass::WrapperDescriptorType .to_instance(db) .is_subtype_of(db, target), @@ -847,6 +862,13 @@ impl<'db> Type<'db> { } } + (Type::PropertyInstance(_), _) => KnownClass::Property + .to_instance(db) + .is_subtype_of(db, target), + (_, Type::PropertyInstance(_)) => { + self.is_subtype_of(db, KnownClass::Property.to_instance(db)) + } + // Other than the special cases enumerated above, // `Instance` types are never subtypes of any other variants (Type::Instance(_), _) => false, @@ -1149,8 +1171,8 @@ impl<'db> Type<'db> { | Type::SliceLiteral(..) | Type::FunctionLiteral(..) | Type::BoundMethod(..) - | Type::MethodWrapperDunderGet(..) - | Type::WrapperDescriptorDunderGet + | Type::MethodWrapper(..) + | Type::WrapperDescriptor(..) | Type::ModuleLiteral(..) | Type::ClassLiteral(..) | Type::KnownInstance(..)), @@ -1161,8 +1183,8 @@ impl<'db> Type<'db> { | Type::SliceLiteral(..) | Type::FunctionLiteral(..) | Type::BoundMethod(..) - | Type::MethodWrapperDunderGet(..) - | Type::WrapperDescriptorDunderGet + | Type::MethodWrapper(..) + | Type::WrapperDescriptor(..) | Type::ModuleLiteral(..) | Type::ClassLiteral(..) | Type::KnownInstance(..)), @@ -1178,8 +1200,8 @@ impl<'db> Type<'db> { | Type::BytesLiteral(..) | Type::FunctionLiteral(..) | Type::BoundMethod(..) - | Type::MethodWrapperDunderGet(..) - | Type::WrapperDescriptorDunderGet + | Type::MethodWrapper(..) + | Type::WrapperDescriptor(..) | Type::IntLiteral(..) | Type::SliceLiteral(..) | Type::StringLiteral(..) @@ -1192,8 +1214,8 @@ impl<'db> Type<'db> { | Type::BytesLiteral(..) | Type::FunctionLiteral(..) | Type::BoundMethod(..) - | Type::MethodWrapperDunderGet(..) - | Type::WrapperDescriptorDunderGet + | Type::MethodWrapper(..) + | Type::WrapperDescriptor(..) | Type::IntLiteral(..) | Type::SliceLiteral(..) | Type::StringLiteral(..) @@ -1223,8 +1245,8 @@ impl<'db> Type<'db> { | Type::SliceLiteral(..) | Type::FunctionLiteral(..) | Type::BoundMethod(..) - | Type::MethodWrapperDunderGet(..) - | Type::WrapperDescriptorDunderGet + | Type::MethodWrapper(..) + | Type::WrapperDescriptor(..) | Type::ModuleLiteral(..), ) | ( @@ -1236,8 +1258,8 @@ impl<'db> Type<'db> { | Type::SliceLiteral(..) | Type::FunctionLiteral(..) | Type::BoundMethod(..) - | Type::MethodWrapperDunderGet(..) - | Type::WrapperDescriptorDunderGet + | Type::MethodWrapper(..) + | Type::WrapperDescriptor(..) | Type::ModuleLiteral(..), Type::SubclassOf(_), ) => true, @@ -1346,16 +1368,17 @@ impl<'db> Type<'db> { .to_instance(db) .is_disjoint_from(db, other), - (Type::MethodWrapperDunderGet(_), other) | (other, Type::MethodWrapperDunderGet(_)) => { + (Type::MethodWrapper(_), other) | (other, Type::MethodWrapper(_)) => { KnownClass::MethodWrapperType .to_instance(db) .is_disjoint_from(db, other) } - (Type::WrapperDescriptorDunderGet, other) - | (other, Type::WrapperDescriptorDunderGet) => KnownClass::WrapperDescriptorType - .to_instance(db) - .is_disjoint_from(db, other), + (Type::WrapperDescriptor(_), other) | (other, Type::WrapperDescriptor(_)) => { + KnownClass::WrapperDescriptorType + .to_instance(db) + .is_disjoint_from(db, other) + } (Type::Callable(_) | Type::FunctionLiteral(_), Type::Callable(_)) | (Type::Callable(_), Type::FunctionLiteral(_)) => { @@ -1406,6 +1429,10 @@ impl<'db> Type<'db> { // TODO: add checks for the above cases once we support them instance.is_disjoint_from(db, KnownClass::Tuple.to_instance(db)) } + + (Type::PropertyInstance(_), _) | (_, Type::PropertyInstance(_)) => KnownClass::Property + .to_instance(db) + .is_disjoint_from(db, other), } } @@ -1416,8 +1443,8 @@ impl<'db> Type<'db> { Type::Never | Type::FunctionLiteral(..) | Type::BoundMethod(_) - | Type::WrapperDescriptorDunderGet - | Type::MethodWrapperDunderGet(_) + | Type::WrapperDescriptor(_) + | Type::MethodWrapper(_) | Type::ModuleLiteral(..) | Type::IntLiteral(_) | Type::BooleanLiteral(_) @@ -1427,7 +1454,8 @@ impl<'db> Type<'db> { | Type::SliceLiteral(_) | Type::KnownInstance(_) | Type::AlwaysFalsy - | Type::AlwaysTruthy => true, + | Type::AlwaysTruthy + | Type::PropertyInstance(_) => true, Type::SubclassOf(subclass_of_ty) => subclass_of_ty.is_fully_static(), Type::ClassLiteral(_) | Type::Instance(_) => { // TODO: Ideally, we would iterate over the MRO of the class, check if all @@ -1483,7 +1511,7 @@ impl<'db> Type<'db> { Type::SubclassOf(..) => false, Type::BooleanLiteral(_) | Type::FunctionLiteral(..) - | Type::WrapperDescriptorDunderGet + | Type::WrapperDescriptor(..) | Type::ClassLiteral(..) | Type::ModuleLiteral(..) | Type::KnownInstance(..) => true, @@ -1504,7 +1532,7 @@ impl<'db> Type<'db> { // ``` false } - Type::MethodWrapperDunderGet(_) => { + Type::MethodWrapper(_) => { // Just a special case of `BoundMethod` really // (this variant represents `f.__get__`, where `f` is any function) false @@ -1512,6 +1540,7 @@ impl<'db> Type<'db> { Type::Instance(InstanceType { class }) => { class.known(db).is_some_and(KnownClass::is_singleton) } + Type::PropertyInstance(_) => false, Type::Tuple(..) => { // The empty tuple is a singleton on CPython and PyPy, but not on other Python // implementations such as GraalPy. Its *use* as a singleton is discouraged and @@ -1545,8 +1574,8 @@ impl<'db> Type<'db> { match self { Type::FunctionLiteral(..) | Type::BoundMethod(_) - | Type::WrapperDescriptorDunderGet - | Type::MethodWrapperDunderGet(_) + | Type::WrapperDescriptor(_) + | Type::MethodWrapper(_) | Type::ModuleLiteral(..) | Type::ClassLiteral(..) | Type::IntLiteral(..) @@ -1577,7 +1606,8 @@ impl<'db> Type<'db> { | Type::LiteralString | Type::AlwaysTruthy | Type::AlwaysFalsy - | Type::Callable(_) => false, + | Type::Callable(_) + | Type::PropertyInstance(_) => false, } } @@ -1610,13 +1640,28 @@ impl<'db> Type<'db> { Type::ClassLiteral(class_literal @ ClassLiteralType { class }) => { match (class.known(db), name) { - (Some(KnownClass::FunctionType), "__get__") => { - Some(Symbol::bound(Type::WrapperDescriptorDunderGet).into()) - } + (Some(KnownClass::FunctionType), "__get__") => Some( + Symbol::bound(Type::WrapperDescriptor( + WrapperDescriptorKind::FunctionTypeDunderGet, + )) + .into(), + ), (Some(KnownClass::FunctionType), "__set__" | "__delete__") => { // Hard code this knowledge, as we look up `__set__` and `__delete__` on `FunctionType` often. Some(Symbol::Unbound.into()) } + (Some(KnownClass::Property), "__get__") => Some( + Symbol::bound(Type::WrapperDescriptor( + WrapperDescriptorKind::PropertyDunderGet, + )) + .into(), + ), + (Some(KnownClass::Property), "__set__") => Some( + Symbol::bound(Type::WrapperDescriptor( + WrapperDescriptorKind::PropertyDunderSet, + )) + .into(), + ), // 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 @@ -1674,8 +1719,8 @@ impl<'db> Type<'db> { Type::FunctionLiteral(_) | Type::Callable(_) | Type::BoundMethod(_) - | Type::WrapperDescriptorDunderGet - | Type::MethodWrapperDunderGet(_) + | Type::WrapperDescriptor(_) + | Type::MethodWrapper(_) | Type::ModuleLiteral(_) | Type::KnownInstance(_) | Type::AlwaysTruthy @@ -1687,7 +1732,8 @@ impl<'db> Type<'db> { | Type::BytesLiteral(_) | Type::SliceLiteral(_) | Type::Tuple(_) - | Type::Instance(_) => None, + | Type::Instance(_) + | Type::PropertyInstance(_) => None, } } @@ -1749,10 +1795,10 @@ impl<'db> Type<'db> { Type::BoundMethod(_) => KnownClass::MethodType .to_instance(db) .instance_member(db, name), - Type::MethodWrapperDunderGet(_) => KnownClass::MethodWrapperType + Type::MethodWrapper(_) => KnownClass::MethodWrapperType .to_instance(db) .instance_member(db, name), - Type::WrapperDescriptorDunderGet => KnownClass::WrapperDescriptorType + Type::WrapperDescriptor(_) => KnownClass::WrapperDescriptorType .to_instance(db) .instance_member(db, name), Type::Callable(_) => KnownClass::Object.to_instance(db).instance_member(db, name), @@ -1773,6 +1819,10 @@ impl<'db> Type<'db> { Type::KnownInstance(_) => Symbol::Unbound.into(), + Type::PropertyInstance(_) => KnownClass::Property + .to_instance(db) + .instance_member(db, name), + // TODO: we currently don't model the fact that class literals and subclass-of types have // a `__dict__` that is filled with class level attributes. Modeling this is currently not // required, as `instance_member` is only called for instance-like types through `member`, @@ -2076,16 +2126,43 @@ impl<'db> Type<'db> { Type::Dynamic(..) | Type::Never => Symbol::bound(self).into(), - Type::FunctionLiteral(function) if name == "__get__" => { - Symbol::bound(Type::MethodWrapperDunderGet(function)).into() - } + Type::FunctionLiteral(function) if name == "__get__" => Symbol::bound( + Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)), + ) + .into(), + Type::PropertyInstance(property) if name == "__get__" => Symbol::bound( + Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(property)), + ) + .into(), + Type::PropertyInstance(property) if name == "__set__" => Symbol::bound( + Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(property)), + ) + .into(), Type::ClassLiteral(ClassLiteralType { class }) if name == "__get__" && class.is_known(db, KnownClass::FunctionType) => { - Symbol::bound(Type::WrapperDescriptorDunderGet).into() + Symbol::bound(Type::WrapperDescriptor( + WrapperDescriptorKind::FunctionTypeDunderGet, + )) + .into() + } + Type::ClassLiteral(ClassLiteralType { class }) + if name == "__get__" && class.is_known(db, KnownClass::Property) => + { + Symbol::bound(Type::WrapperDescriptor( + WrapperDescriptorKind::PropertyDunderGet, + )) + .into() + } + Type::ClassLiteral(ClassLiteralType { class }) + if name == "__set__" && class.is_known(db, KnownClass::Property) => + { + Symbol::bound(Type::WrapperDescriptor( + WrapperDescriptorKind::PropertyDunderSet, + )) + .into() } - Type::BoundMethod(bound_method) => match name_str { "__self__" => Symbol::bound(bound_method.self_instance(db)).into(), "__func__" => { @@ -2102,10 +2179,10 @@ impl<'db> Type<'db> { }) } }, - Type::MethodWrapperDunderGet(_) => KnownClass::MethodWrapperType + Type::MethodWrapper(_) => KnownClass::MethodWrapperType .to_instance(db) .member(db, &name), - Type::WrapperDescriptorDunderGet => KnownClass::WrapperDescriptorType + Type::WrapperDescriptor(_) => KnownClass::WrapperDescriptorType .to_instance(db) .member(db, &name), Type::Callable(_) => KnownClass::Object.to_instance(db).member(db, &name), @@ -2127,6 +2204,13 @@ impl<'db> Type<'db> { SymbolAndQualifiers::todo("super() support") } + Type::PropertyInstance(property) if name == "fget" => { + Symbol::bound(property.getter(db).unwrap_or(Type::none(db))).into() + } + Type::PropertyInstance(property) if name == "fset" => { + Symbol::bound(property.setter(db).unwrap_or(Type::none(db))).into() + } + Type::IntLiteral(_) if matches!(name_str, "real" | "numerator") => { Symbol::bound(self).into() } @@ -2156,6 +2240,7 @@ impl<'db> Type<'db> { | Type::SliceLiteral(..) | Type::Tuple(..) | Type::KnownInstance(..) + | Type::PropertyInstance(..) | Type::FunctionLiteral(..) => { let fallback = self.instance_member(db, name_str); @@ -2280,8 +2365,8 @@ impl<'db> Type<'db> { Type::FunctionLiteral(_) | Type::BoundMethod(_) - | Type::WrapperDescriptorDunderGet - | Type::MethodWrapperDunderGet(_) + | Type::WrapperDescriptor(_) + | Type::MethodWrapper(_) | Type::ModuleLiteral(_) | Type::SliceLiteral(_) | Type::AlwaysTruthy => Truthiness::AlwaysTrue, @@ -2366,6 +2451,8 @@ impl<'db> Type<'db> { Type::KnownInstance(known_instance) => known_instance.bool(), + Type::PropertyInstance(_) => Truthiness::AlwaysTrue, + Type::Union(union) => { let mut truthiness = None; let mut all_not_callable = true; @@ -2489,7 +2576,10 @@ impl<'db> Type<'db> { Signatures::single(signature) } - Type::MethodWrapperDunderGet(_) => { + Type::MethodWrapper( + MethodWrapperKind::FunctionTypeDunderGet(_) + | MethodWrapperKind::PropertyDunderGet(_), + ) => { // 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: @@ -2502,6 +2592,9 @@ impl<'db> Type<'db> { // @overload // def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ... // ``` + // + // For `builtins.property.__get__`, we use the same signature. The return types are not + // specified yet, they will be dynamically added in `Bindings::evaluate_known_cases`. let not_none = Type::none(db).negate(db); let signature = CallableSignature::from_overloads( @@ -2534,21 +2627,36 @@ impl<'db> Type<'db> { Signatures::single(signature) } - Type::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. + Type::WrapperDescriptor( + kind @ (WrapperDescriptorKind::FunctionTypeDunderGet + | WrapperDescriptorKind::PropertyDunderGet), + ) => { + // Here, we also model `types.FunctionType.__get__` (or builtins.property.__get__), + // but now we consider a call to this as a function, i.e. we also expect the `self` + // argument to be passed in. // TODO: Consider merging this signature with the one in the previous match clause, // since the previous one is just this signature with the `self` parameters // removed. let not_none = Type::none(db).negate(db); + let descriptor = match kind { + WrapperDescriptorKind::FunctionTypeDunderGet => { + KnownClass::FunctionType.to_instance(db) + } + WrapperDescriptorKind::PropertyDunderGet => { + KnownClass::Property.to_instance(db) + } + WrapperDescriptorKind::PropertyDunderSet => { + unreachable!("Not part of outer match pattern") + } + }; let signature = CallableSignature::from_overloads( self, [ Signature::new( Parameters::new([ Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(KnownClass::FunctionType.to_instance(db)), + .with_annotated_type(descriptor), Parameter::positional_only(Some(Name::new_static("instance"))) .with_annotated_type(Type::none(db)), Parameter::positional_only(Some(Name::new_static("owner"))) @@ -2559,7 +2667,7 @@ impl<'db> Type<'db> { Signature::new( Parameters::new([ Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(KnownClass::FunctionType.to_instance(db)), + .with_annotated_type(descriptor), Parameter::positional_only(Some(Name::new_static("instance"))) .with_annotated_type(not_none), Parameter::positional_only(Some(Name::new_static("owner"))) @@ -2576,6 +2684,37 @@ impl<'db> Type<'db> { Signatures::single(signature) } + Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(_)) => { + Signatures::single(CallableSignature::single( + self, + Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("instance"))) + .with_annotated_type(Type::object(db)), + Parameter::positional_only(Some(Name::new_static("value"))) + .with_annotated_type(Type::object(db)), + ]), + None, + ), + )) + } + Type::WrapperDescriptor(WrapperDescriptorKind::PropertyDunderSet) => { + Signatures::single(CallableSignature::single( + self, + Signature::new( + Parameters::new([ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(KnownClass::Property.to_instance(db)), + Parameter::positional_only(Some(Name::new_static("instance"))) + .with_annotated_type(Type::object(db)), + Parameter::positional_only(Some(Name::new_static("value"))) + .with_annotated_type(Type::object(db)), + ]), + None, + ), + )) + } + Type::FunctionLiteral(function_type) => match function_type.known(db) { Some( KnownFunction::IsEquivalentTo @@ -2749,6 +2888,74 @@ impl<'db> Type<'db> { Signatures::single(signature) } + Some(KnownClass::Property) => { + let getter_signature = Signature::new( + Parameters::new([ + Parameter::positional_only(None).with_annotated_type(Type::any()) + ]), + Some(Type::any()), + ); + let setter_signature = Signature::new( + Parameters::new([ + Parameter::positional_only(None).with_annotated_type(Type::any()), + Parameter::positional_only(None).with_annotated_type(Type::any()), + ]), + Some(Type::none(db)), + ); + let deleter_signature = Signature::new( + Parameters::new([ + Parameter::positional_only(None).with_annotated_type(Type::any()) + ]), + Some(Type::any()), + ); + + let signature = CallableSignature::single( + self, + Signature::new( + Parameters::new([ + Parameter::positional_or_keyword(Name::new_static("fget")) + .with_annotated_type(UnionType::from_elements( + db, + [ + Type::Callable(CallableType::new(db, getter_signature)), + Type::none(db), + ], + )) + .with_default_type(Type::none(db)), + Parameter::positional_or_keyword(Name::new_static("fset")) + .with_annotated_type(UnionType::from_elements( + db, + [ + Type::Callable(CallableType::new(db, setter_signature)), + Type::none(db), + ], + )) + .with_default_type(Type::none(db)), + Parameter::positional_or_keyword(Name::new_static("fdel")) + .with_annotated_type(UnionType::from_elements( + db, + [ + Type::Callable(CallableType::new( + db, + deleter_signature, + )), + Type::none(db), + ], + )) + .with_default_type(Type::none(db)), + Parameter::positional_or_keyword(Name::new_static("doc")) + .with_annotated_type(UnionType::from_elements( + db, + [KnownClass::Str.to_instance(db), Type::none(db)], + )) + .with_default_type(Type::none(db)), + ]), + None, + ), + ); + Signatures::single(signature) + } + // TODO annotated return type on `__new__` or metaclass `__call__` // TODO check call vs signatures of `__new__` and/or `__init__` _ => { @@ -3018,11 +3225,12 @@ impl<'db> Type<'db> { | Type::BytesLiteral(_) | Type::FunctionLiteral(_) | Type::Callable(..) - | Type::MethodWrapperDunderGet(_) + | Type::MethodWrapper(_) | Type::BoundMethod(_) - | Type::WrapperDescriptorDunderGet + | Type::WrapperDescriptor(_) | Type::Instance(_) | Type::KnownInstance(_) + | Type::PropertyInstance(_) | Type::ModuleLiteral(_) | Type::IntLiteral(_) | Type::StringLiteral(_) @@ -3083,10 +3291,11 @@ impl<'db> Type<'db> { | Type::Tuple(_) | Type::Callable(_) | Type::BoundMethod(_) - | Type::WrapperDescriptorDunderGet - | Type::MethodWrapperDunderGet(_) + | Type::WrapperDescriptor(_) + | Type::MethodWrapper(_) | Type::Never - | Type::FunctionLiteral(_) => Err(InvalidTypeExpressionError { + | Type::FunctionLiteral(_) + | Type::PropertyInstance(_) => Err(InvalidTypeExpressionError { invalid_expressions: smallvec::smallvec![InvalidTypeExpression::InvalidType(*self)], fallback_type: Type::unknown(), }), @@ -3281,6 +3490,7 @@ impl<'db> Type<'db> { Type::Never => Type::Never, Type::Instance(InstanceType { class }) => SubclassOfType::from(db, *class), Type::KnownInstance(known_instance) => known_instance.class().to_class_literal(db), + Type::PropertyInstance(_) => KnownClass::Property.to_class_literal(db), Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)), Type::BooleanLiteral(_) => KnownClass::Bool.to_class_literal(db), Type::BytesLiteral(_) => KnownClass::Bytes.to_class_literal(db), @@ -3288,10 +3498,8 @@ impl<'db> Type<'db> { Type::IntLiteral(_) => KnownClass::Int.to_class_literal(db), Type::FunctionLiteral(_) => KnownClass::FunctionType.to_class_literal(db), Type::BoundMethod(_) => KnownClass::MethodType.to_class_literal(db), - Type::MethodWrapperDunderGet(_) => KnownClass::MethodWrapperType.to_class_literal(db), - Type::WrapperDescriptorDunderGet => { - KnownClass::WrapperDescriptorType.to_class_literal(db) - } + Type::MethodWrapper(_) => KnownClass::MethodWrapperType.to_class_literal(db), + Type::WrapperDescriptor(_) => KnownClass::WrapperDescriptorType.to_class_literal(db), Type::Callable(_) => KnownClass::Type.to_instance(db), Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db), Type::Tuple(_) => KnownClass::Tuple.to_class_literal(db), @@ -4213,6 +4421,18 @@ impl From for Truthiness { } } +bitflags! { + #[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Hash)] + pub struct FunctionDecorators: u8 { + /// `@classmethod` + const CLASSMETHOD = 1 << 0; + /// `@no_type_check` + const NO_TYPE_CHECK = 1 << 1; + /// `@overload` + const OVERLOAD = 1 << 2; + } +} + #[salsa::interned(debug)] pub struct FunctionType<'db> { /// name of the function at definition @@ -4224,24 +4444,14 @@ pub struct FunctionType<'db> { body_scope: ScopeId<'db>, - /// types of all decorators on this function - decorators: Box<[Type<'db>]>, + /// A set of special decorators that were applied to this function + decorators: FunctionDecorators, } #[salsa::tracked] impl<'db> FunctionType<'db> { - pub fn has_known_class_decorator(self, db: &dyn Db, decorator: KnownClass) -> bool { - self.decorators(db).iter().any(|d| { - d.into_class_literal() - .is_some_and(|c| c.class.is_known(db, decorator)) - }) - } - - pub fn has_known_function_decorator(self, db: &dyn Db, decorator: KnownFunction) -> bool { - self.decorators(db).iter().any(|d| { - d.into_function_literal() - .is_some_and(|f| f.is_known(db, decorator)) - }) + pub fn has_known_decorator(self, db: &dyn Db, decorator: FunctionDecorators) -> bool { + self.decorators(db).contains(decorator) } /// Convert the `FunctionType` into a [`Type::Callable`]. @@ -4267,18 +4477,8 @@ impl<'db> FunctionType<'db> { pub fn signature(self, db: &'db dyn Db) -> Signature<'db> { let internal_signature = self.internal_signature(db); - let decorators = self.decorators(db); - let mut decorators = decorators.iter(); - - if let Some(d) = decorators.next() { - if d.into_class_literal() - .is_some_and(|c| c.class.is_known(db, KnownClass::Classmethod)) - && decorators.next().is_none() - { - internal_signature - } else { - Signature::todo("return type of decorated function") - } + if self.has_known_decorator(db, FunctionDecorators::OVERLOAD) { + Signature::todo("return type of overloaded function") } else { internal_signature } @@ -4993,6 +5193,28 @@ impl<'db> CallableType<'db> { } } +/// Represents a specific instance of `types.MethodWrapperType` +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, salsa::Update)] +pub enum MethodWrapperKind<'db> { + /// Method wrapper for `some_function.__get__` + FunctionTypeDunderGet(FunctionType<'db>), + /// Method wrapper for `some_property.__get__` + PropertyDunderGet(PropertyInstanceType<'db>), + /// Method wrapper for `some_property.__set__` + PropertyDunderSet(PropertyInstanceType<'db>), +} + +/// Represents a specific instance of `types.WrapperDescriptorType` +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, salsa::Update)] +pub enum WrapperDescriptorKind { + /// `FunctionType.__get__` + FunctionTypeDunderGet, + /// `property.__get__` + PropertyDunderGet, + /// `property.__set__` + PropertyDunderSet, +} + #[salsa::interned(debug)] pub struct ModuleLiteralType<'db> { /// The file in which this module was imported. 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 ee1a829348..9de9047029 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -18,8 +18,8 @@ use crate::types::diagnostic::{ }; use crate::types::signatures::{Parameter, ParameterForm}; use crate::types::{ - todo_type, BoundMethodType, ClassLiteralType, KnownClass, KnownFunction, KnownInstanceType, - UnionType, + todo_type, BoundMethodType, ClassLiteralType, FunctionDecorators, KnownClass, KnownFunction, + KnownInstanceType, MethodWrapperKind, PropertyInstanceType, UnionType, WrapperDescriptorKind, }; use ruff_db::diagnostic::{OldSecondaryDiagnosticMessage, Span}; use ruff_python_ast as ast; @@ -210,10 +210,8 @@ impl<'db> Bindings<'db> { }; match binding_type { - Type::MethodWrapperDunderGet(function) => { - if function.has_known_class_decorator(db, KnownClass::Classmethod) - && function.decorators(db).len() == 1 - { + Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { + if function.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) { match overload.parameter_types() { [_, Some(owner)] => { overload.set_return_type(Type::BoundMethod(BoundMethodType::new( @@ -240,13 +238,11 @@ impl<'db> Bindings<'db> { } } - Type::WrapperDescriptorDunderGet => { + Type::WrapperDescriptor(WrapperDescriptorKind::FunctionTypeDunderGet) => { if let [Some(function_ty @ Type::FunctionLiteral(function)), ..] = overload.parameter_types() { - if function.has_known_class_decorator(db, KnownClass::Classmethod) - && function.decorators(db).len() == 1 - { + if function.has_known_decorator(db, FunctionDecorators::CLASSMETHOD) { match overload.parameter_types() { [_, _, Some(owner)] => { overload.set_return_type(Type::BoundMethod( @@ -271,36 +267,6 @@ impl<'db> Bindings<'db> { [_, Some(instance), _] if instance.is_none(db) => { overload.set_return_type(*function_ty); } - - [_, Some(Type::KnownInstance(KnownInstanceType::TypeAliasType( - type_alias, - ))), Some(Type::ClassLiteral(ClassLiteralType { class }))] - if class.is_known(db, KnownClass::TypeAliasType) - && function.name(db) == "__name__" => - { - overload.set_return_type(Type::string_literal( - db, - type_alias.name(db), - )); - } - - [_, Some(Type::KnownInstance(KnownInstanceType::TypeVar(typevar))), Some(Type::ClassLiteral(ClassLiteralType { class }))] - if class.is_known(db, KnownClass::TypeVar) - && function.name(db) == "__name__" => - { - overload.set_return_type(Type::string_literal( - db, - typevar.name(db), - )); - } - - [_, Some(_), _] - if function - .has_known_class_decorator(db, KnownClass::Property) => - { - overload.set_return_type(todo_type!("@property")); - } - [_, Some(instance), _] => { overload.set_return_type(Type::BoundMethod( BoundMethodType::new(db, *function, *instance), @@ -313,6 +279,165 @@ impl<'db> Bindings<'db> { } } + Type::WrapperDescriptor(WrapperDescriptorKind::PropertyDunderGet) => { + match overload.parameter_types() { + [Some(property @ Type::PropertyInstance(_)), Some(instance), ..] + if instance.is_none(db) => + { + overload.set_return_type(*property); + } + [Some(Type::PropertyInstance(property)), Some(Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias))), ..] + if property.getter(db).is_some_and(|getter| { + getter + .into_function_literal() + .is_some_and(|f| f.name(db) == "__name__") + }) => + { + overload.set_return_type(Type::string_literal(db, type_alias.name(db))); + } + [Some(Type::PropertyInstance(property)), Some(Type::KnownInstance(KnownInstanceType::TypeVar(type_var))), ..] + if property.getter(db).is_some_and(|getter| { + getter + .into_function_literal() + .is_some_and(|f| f.name(db) == "__name__") + }) => + { + overload.set_return_type(Type::string_literal(db, type_var.name(db))); + } + [Some(Type::PropertyInstance(property)), Some(instance), ..] => { + if let Some(getter) = property.getter(db) { + if let Ok(return_ty) = getter + .try_call(db, CallArgumentTypes::positional([*instance])) + .map(|binding| binding.return_type(db)) + { + overload.set_return_type(return_ty); + } else { + overload.errors.push(BindingError::InternalCallError( + "calling the getter failed", + )); + overload.set_return_type(Type::unknown()); + } + } else { + overload.errors.push(BindingError::InternalCallError( + "property has no getter", + )); + overload.set_return_type(Type::Never); + } + } + _ => {} + } + } + + Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(property)) => { + match overload.parameter_types() { + [Some(instance), ..] if instance.is_none(db) => { + overload.set_return_type(Type::PropertyInstance(property)); + } + [Some(instance), ..] => { + if let Some(getter) = property.getter(db) { + if let Ok(return_ty) = getter + .try_call(db, CallArgumentTypes::positional([*instance])) + .map(|binding| binding.return_type(db)) + { + overload.set_return_type(return_ty); + } else { + overload.errors.push(BindingError::InternalCallError( + "calling the getter failed", + )); + overload.set_return_type(Type::unknown()); + } + } else { + overload.set_return_type(Type::Never); + overload.errors.push(BindingError::InternalCallError( + "property has no getter", + )); + } + } + _ => {} + } + } + + Type::WrapperDescriptor(WrapperDescriptorKind::PropertyDunderSet) => { + if let [Some(Type::PropertyInstance(property)), Some(instance), Some(value), ..] = + overload.parameter_types() + { + if let Some(setter) = property.setter(db) { + if let Err(_call_error) = setter + .try_call(db, CallArgumentTypes::positional([*instance, *value])) + { + overload.errors.push(BindingError::InternalCallError( + "calling the setter failed", + )); + } + } else { + overload + .errors + .push(BindingError::InternalCallError("property has no setter")); + } + } + } + + Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(property)) => { + if let [Some(instance), Some(value), ..] = overload.parameter_types() { + if let Some(setter) = property.setter(db) { + if let Err(_call_error) = setter + .try_call(db, CallArgumentTypes::positional([*instance, *value])) + { + overload.errors.push(BindingError::InternalCallError( + "calling the setter failed", + )); + } + } else { + overload + .errors + .push(BindingError::InternalCallError("property has no setter")); + } + } + } + + Type::BoundMethod(bound_method) + if bound_method.self_instance(db).is_property_instance() => + { + match bound_method.function(db).name(db).as_str() { + "setter" => { + if let [Some(_), Some(setter)] = overload.parameter_types() { + let mut ty_property = bound_method.self_instance(db); + if let Type::PropertyInstance(property) = ty_property { + ty_property = + Type::PropertyInstance(PropertyInstanceType::new( + db, + property.getter(db), + Some(*setter), + )); + } + overload.set_return_type(ty_property); + } + } + "getter" => { + if let [Some(_), Some(getter)] = overload.parameter_types() { + let mut ty_property = bound_method.self_instance(db); + if let Type::PropertyInstance(property) = ty_property { + ty_property = + Type::PropertyInstance(PropertyInstanceType::new( + db, + Some(*getter), + property.setter(db), + )); + } + overload.set_return_type(ty_property); + } + } + "deleter" => { + // TODO: we do not store deleters yet + let ty_property = bound_method.self_instance(db); + overload.set_return_type(ty_property); + } + _ => { + // Fall back to typeshed stubs for all other methods + } + } + } + Type::FunctionLiteral(function_type) => match function_type.known(db) { Some(KnownFunction::IsEquivalentTo) => { if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() { @@ -462,6 +587,14 @@ impl<'db> Bindings<'db> { } } + Some(KnownClass::Property) => { + if let [getter, setter, ..] = overload.parameter_types() { + overload.set_return_type(Type::PropertyInstance( + PropertyInstanceType::new(db, *getter, *setter), + )); + } + } + _ => {} }, @@ -931,13 +1064,25 @@ impl<'db> CallableDescription<'db> { kind: "bound method", name: bound_method.function(db).name(db), }), - Type::MethodWrapperDunderGet(function) => Some(CallableDescription { - kind: "method wrapper `__get__` of function", - name: function.name(db), - }), - Type::WrapperDescriptorDunderGet => Some(CallableDescription { + Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { + Some(CallableDescription { + kind: "method wrapper `__get__` of function", + name: function.name(db), + }) + } + Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(_)) => { + Some(CallableDescription { + kind: "method wrapper", + name: "`__get__` of property", + }) + } + Type::WrapperDescriptor(kind) => Some(CallableDescription { kind: "wrapper descriptor", - name: "FunctionType.__get__", + name: match kind { + WrapperDescriptorKind::FunctionTypeDunderGet => "FunctionType.__get__", + WrapperDescriptorKind::PropertyDunderGet => "property.__get__", + WrapperDescriptorKind::PropertyDunderSet => "property.__set__", + }, }), _ => None, } @@ -1025,6 +1170,10 @@ pub(crate) enum BindingError<'db> { argument_index: Option, parameter: ParameterContext, }, + /// The call itself might be well constructed, but an error occurred while evaluating the call. + /// We use this variant to report errors in `property.__get__` and `property.__set__`, which + /// can occur when the call to the underlying getter/setter fails. + InternalCallError(&'static str), } impl<'db> BindingError<'db> { @@ -1173,6 +1322,21 @@ impl<'db> BindingError<'db> { ), ); } + + Self::InternalCallError(reason) => { + context.report_lint( + &CALL_NON_CALLABLE, + Self::get_node(node, None), + format_args!( + "Call{} failed: {reason}", + if let Some(CallableDescription { kind, name }) = callable_description { + format!(" of {kind} `{name}`") + } else { + String::new() + } + ), + ); + } } } 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 177ccd1dd8..d789cad2e3 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -69,13 +69,14 @@ impl<'db> ClassBase<'db> { Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs? Type::Intersection(_) => None, // TODO -- probably incorrect? Type::Instance(_) => None, // TODO -- handle `__mro_entries__`? + Type::PropertyInstance(_) => None, Type::Never | Type::BooleanLiteral(_) | Type::FunctionLiteral(_) | Type::Callable(..) | Type::BoundMethod(_) - | Type::MethodWrapperDunderGet(_) - | Type::WrapperDescriptorDunderGet + | Type::MethodWrapper(_) + | Type::WrapperDescriptor(_) | Type::BytesLiteral(_) | Type::IntLiteral(_) | Type::StringLiteral(_) diff --git a/crates/red_knot_python_semantic/src/types/context.rs b/crates/red_knot_python_semantic/src/types/context.rs index b35cfcc299..3e7a47b1ed 100644 --- a/crates/red_knot_python_semantic/src/types/context.rs +++ b/crates/red_knot_python_semantic/src/types/context.rs @@ -7,15 +7,15 @@ use ruff_db::{ }; use ruff_text_size::{Ranged, TextRange}; -use super::{binding_type, KnownFunction, Type, TypeCheckDiagnostic, TypeCheckDiagnostics}; +use super::{binding_type, Type, TypeCheckDiagnostic, TypeCheckDiagnostics}; -use crate::semantic_index::semantic_index; use crate::semantic_index::symbol::ScopeId; use crate::{ lint::{LintId, LintMetadata}, suppression::suppressions, Db, }; +use crate::{semantic_index::semantic_index, types::FunctionDecorators}; /// Context for inferring the types of a single file. /// @@ -182,13 +182,7 @@ impl<'db> InferContext<'db> { // Iterate over all functions and test if any is decorated with `@no_type_check`. function_scope_tys.any(|function_ty| { - function_ty - .decorators(self.db) - .iter() - .filter_map(|decorator| decorator.into_function_literal()) - .any(|decorator_ty| { - decorator_ty.is_known(self.db, KnownFunction::NoTypeCheck) - }) + function_ty.has_known_decorator(self.db, FunctionDecorators::NO_TYPE_CHECK) }) } InNoTypeCheck::Yes => true, diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 963da7871d..ceeed236c6 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -9,8 +9,8 @@ use ruff_python_literal::escape::AsciiEscape; use crate::types::class_base::ClassBase; use crate::types::signatures::{Parameter, Parameters, Signature}; use crate::types::{ - ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType, Type, - UnionType, + ClassLiteralType, InstanceType, IntersectionType, KnownClass, MethodWrapperKind, + StringLiteralType, Type, UnionType, WrapperDescriptorKind, }; use crate::Db; use rustc_hash::FxHashMap; @@ -77,6 +77,7 @@ impl Display for DisplayRepresentation<'_> { }; f.write_str(representation) } + Type::PropertyInstance(_) => f.write_str("property"), Type::ModuleLiteral(module) => { write!(f, "", module.module(self.db).name()) } @@ -99,15 +100,26 @@ impl Display for DisplayRepresentation<'_> { instance = bound_method.self_instance(self.db).display(self.db) ) } - Type::MethodWrapperDunderGet(function) => { + Type::MethodWrapper(MethodWrapperKind::FunctionTypeDunderGet(function)) => { write!( f, "", function = function.name(self.db) ) } - Type::WrapperDescriptorDunderGet => { - f.write_str("") + Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(_)) => { + write!(f, "",) + } + Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(_)) => { + write!(f, "",) + } + Type::WrapperDescriptor(kind) => { + let (method, object) = match kind { + WrapperDescriptorKind::FunctionTypeDunderGet => ("__get__", "function"), + WrapperDescriptorKind::PropertyDunderGet => ("__get__", "property"), + WrapperDescriptorKind::PropertyDunderSet => ("__set__", "property"), + }; + write!(f, "") } Type::Union(union) => union.display(self.db).fmt(f), Type::Intersection(intersection) => intersection.display(self.db).fmt(f), @@ -421,7 +433,7 @@ struct DisplayMaybeParenthesizedType<'db> { impl Display for DisplayMaybeParenthesizedType<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - if let Type::Callable(_) | Type::MethodWrapperDunderGet(_) = self.ty { + if let Type::Callable(_) | Type::MethodWrapper(_) = self.ty { write!(f, "({})", self.ty.display(self.db)) } else { self.ty.display(self.db).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 b06b1fe96a..c7bf461bbb 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -82,7 +82,7 @@ use crate::types::{ Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType, }; -use crate::types::{CallableType, Signature}; +use crate::types::{CallableType, FunctionDecorators, Signature}; use crate::unpack::{Unpack, UnpackPosition}; use crate::util::subscript::{PyIndex, PySlice}; use crate::Db; @@ -1373,19 +1373,31 @@ impl<'db> TypeInferenceBuilder<'db> { decorator_list, } = function; - // Check if the function is decorated with the `no_type_check` decorator - // and, if so, suppress any errors that come after the decorators. - let mut decorator_tys = Vec::with_capacity(decorator_list.len()); + let mut decorator_types_and_nodes = Vec::with_capacity(decorator_list.len()); + let mut function_decorators = FunctionDecorators::empty(); for decorator in decorator_list { - let ty = self.infer_decorator(decorator); - decorator_tys.push(ty); + let decorator_ty = self.infer_decorator(decorator); - if let Type::FunctionLiteral(function) = ty { + if let Type::FunctionLiteral(function) = decorator_ty { if function.is_known(self.db(), KnownFunction::NoTypeCheck) { + // If the function is decorated with the `no_type_check` decorator, + // we need to suppress any errors that come after the decorators. self.context.set_in_no_type_check(InNoTypeCheck::Yes); + function_decorators |= FunctionDecorators::NO_TYPE_CHECK; + continue; + } else if function.is_known(self.db(), KnownFunction::Overload) { + function_decorators |= FunctionDecorators::OVERLOAD; + continue; + } + } else if let Type::ClassLiteral(class) = decorator_ty { + if class.class.is_known(self.db(), KnownClass::Classmethod) { + function_decorators |= FunctionDecorators::CLASSMETHOD; + continue; } } + + decorator_types_and_nodes.push((decorator_ty, decorator)); } for default in parameters @@ -1417,18 +1429,31 @@ impl<'db> TypeInferenceBuilder<'db> { .node_scope(NodeWithScopeRef::Function(function)) .to_scope_id(self.db(), self.file()); - let function_ty = Type::FunctionLiteral(FunctionType::new( + let mut inferred_ty = Type::FunctionLiteral(FunctionType::new( self.db(), &name.id, function_kind, body_scope, - decorator_tys.into_boxed_slice(), + function_decorators, )); + for (decorator_ty, decorator_node) in decorator_types_and_nodes.iter().rev() { + inferred_ty = match decorator_ty + .try_call(self.db(), CallArgumentTypes::positional([inferred_ty])) + .map(|bindings| bindings.return_type(self.db())) + { + Ok(return_ty) => return_ty, + Err(CallError(_, bindings)) => { + bindings.report_diagnostics(&self.context, (*decorator_node).into()); + bindings.return_type(self.db()) + } + }; + } + self.add_declaration_with_binding( function.into(), definition, - &DeclaredAndInferredType::AreTheSame(function_ty), + &DeclaredAndInferredType::AreTheSame(inferred_ty), ); } @@ -2311,11 +2336,12 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::SliceLiteral(..) | Type::Tuple(..) | Type::KnownInstance(..) + | Type::PropertyInstance(..) | Type::FunctionLiteral(..) | Type::Callable(..) | Type::BoundMethod(_) - | Type::MethodWrapperDunderGet(_) - | Type::WrapperDescriptorDunderGet + | Type::MethodWrapper(_) + | Type::WrapperDescriptor(_) | Type::AlwaysTruthy | Type::AlwaysFalsy => match object_ty.class_member(db, attribute.into()) { meta_attr @ SymbolAndQualifiers { .. } if meta_attr.is_class_var() => { @@ -4413,14 +4439,15 @@ impl<'db> TypeInferenceBuilder<'db> { op @ (ast::UnaryOp::UAdd | ast::UnaryOp::USub | ast::UnaryOp::Invert), Type::FunctionLiteral(_) | Type::Callable(..) - | Type::WrapperDescriptorDunderGet - | Type::MethodWrapperDunderGet(_) + | Type::WrapperDescriptor(_) + | Type::MethodWrapper(_) | Type::BoundMethod(_) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) | Type::SubclassOf(_) | Type::Instance(_) | Type::KnownInstance(_) + | Type::PropertyInstance(_) | Type::Union(_) | Type::Intersection(_) | Type::AlwaysTruthy @@ -4665,13 +4692,14 @@ impl<'db> TypeInferenceBuilder<'db> { Type::FunctionLiteral(_) | Type::Callable(..) | Type::BoundMethod(_) - | Type::WrapperDescriptorDunderGet - | Type::MethodWrapperDunderGet(_) + | Type::WrapperDescriptor(_) + | Type::MethodWrapper(_) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) | Type::SubclassOf(_) | Type::Instance(_) | Type::KnownInstance(_) + | Type::PropertyInstance(_) | Type::Intersection(_) | Type::AlwaysTruthy | Type::AlwaysFalsy @@ -4684,13 +4712,14 @@ impl<'db> TypeInferenceBuilder<'db> { Type::FunctionLiteral(_) | Type::Callable(..) | Type::BoundMethod(_) - | Type::WrapperDescriptorDunderGet - | Type::MethodWrapperDunderGet(_) + | Type::WrapperDescriptor(_) + | Type::MethodWrapper(_) | Type::ModuleLiteral(_) | Type::ClassLiteral(_) | Type::SubclassOf(_) | Type::Instance(_) | Type::KnownInstance(_) + | Type::PropertyInstance(_) | Type::Intersection(_) | Type::AlwaysTruthy | Type::AlwaysFalsy diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 039f6f4d8d..23ffcdc22c 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -1030,25 +1030,4 @@ mod tests { // With no decorators, internal and external signature are the same assert_eq!(func.signature(&db), &expected_sig); } - - #[test] - fn external_signature_decorated() { - let mut db = setup_db(); - db.write_dedented( - "/src/a.py", - " - def deco(func): ... - - @deco - def f(a: int) -> int: ... - ", - ) - .unwrap(); - let func = get_function_f(&db, "/src/a.py"); - - let expected_sig = Signature::todo("return type of decorated function"); - - // With no decorators, internal and external signature are the same - assert_eq!(func.signature(&db), &expected_sig); - } } 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 596d132fd3..b73b59d57b 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -65,14 +65,13 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::BoundMethod(_), _) => Ordering::Less, (_, Type::BoundMethod(_)) => Ordering::Greater, - (Type::MethodWrapperDunderGet(left), Type::MethodWrapperDunderGet(right)) => { - left.cmp(right) - } - (Type::MethodWrapperDunderGet(_), _) => Ordering::Less, - (_, Type::MethodWrapperDunderGet(_)) => Ordering::Greater, + (Type::MethodWrapper(left), Type::MethodWrapper(right)) => left.cmp(right), + (Type::MethodWrapper(_), _) => Ordering::Less, + (_, Type::MethodWrapper(_)) => Ordering::Greater, - (Type::WrapperDescriptorDunderGet, _) => Ordering::Less, - (_, Type::WrapperDescriptorDunderGet) => Ordering::Greater, + (Type::WrapperDescriptor(left), Type::WrapperDescriptor(right)) => left.cmp(right), + (Type::WrapperDescriptor(_), _) => Ordering::Less, + (_, Type::WrapperDescriptor(_)) => Ordering::Greater, (Type::Callable(left), Type::Callable(right)) => left.cmp(right), (Type::Callable(_), _) => Ordering::Less, @@ -259,6 +258,10 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::KnownInstance(_), _) => Ordering::Less, (_, Type::KnownInstance(_)) => Ordering::Greater, + (Type::PropertyInstance(left), Type::PropertyInstance(right)) => left.cmp(right), + (Type::PropertyInstance(_), _) => Ordering::Less, + (_, Type::PropertyInstance(_)) => Ordering::Greater, + (Type::Dynamic(left), Type::Dynamic(right)) => dynamic_elements_ordering(*left, *right), (Type::Dynamic(_), _) => Ordering::Less, (_, Type::Dynamic(_)) => Ordering::Greater,