[red-knot] Decorators and properties (#17017)

## Summary

Add support for decorators on function as well as support
for properties by adding special handling for `@property` and `@<name of
property>.setter`/`.getter` decorators.

closes https://github.com/astral-sh/ruff/issues/16987

## Ecosystem results

- ✔️ A lot of false positives are fixed by our new
understanding of properties
- 🔴 A bunch of new false positives (typically
`possibly-unbound-attribute` or `invalid-argument-type`) occur because
we currently do not perform type narrowing on attributes. And with the
new understanding of properties, this becomes even more relevant. In
many cases, the narrowing occurs through an assertion, so this is also
something that we need to implement to get rid of these false positives.
- 🔴 A few new false positives occur because we do not
understand generics, and therefore some calls to custom setters fail.
- 🔴 Similarly, some false positives occur because we do not
understand protocols yet.
- ✔️ Seems like a true positive to me. [The
setter](e624d8edfa/src/packaging/specifiers.py (L752-L754))
only accepts `bools`, but `None` is assigned in [this
line](e624d8edfa/tests/test_specifiers.py (L688)).
  ```
+ error[lint:invalid-assignment]
/tmp/mypy_primer/projects/packaging/tests/test_specifiers.py:688:9:
Invalid assignment to data descriptor attribute `prereleases` on type
`SpecifierSet` with custom `__set__` method
  ```
- ✔️ This is arguable also a true positive. The setter
[here](0c6c75644f/rich/table.py (L359-L363))
returns `Table`, but typeshed wants [setters to return
`None`](bf8d2a9912/stdlib/builtins.pyi (L1298)).
  ```
+ error[lint:invalid-argument-type]
/tmp/mypy_primer/projects/rich/rich/table.py:359:5: Object of type
`Literal[padding]` cannot be assigned to parameter 2 (`fset`) of bound
method `setter`; expected type `(Any, Any, /) -> None`
  ```  

## Follow ups

- Fix the `@no_type_check` regression
- Implement class decorators

## Test Plan

New Markdown test suites for decorators and properties.
This commit is contained in:
David Peter 2025-04-02 09:27:46 +02:00 committed by GitHub
parent e1b5b0de71
commit ae2cf91a36
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1201 additions and 261 deletions

View file

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

View file

@ -1541,7 +1541,7 @@ integers are instances of that class:
```py
reveal_type((2).bit_length) # revealed: <bound method `bit_length` of `Literal[2]`>
reveal_type((2).denominator) # revealed: @Todo(@property)
reveal_type((2).denominator) # revealed: Literal[1]
```
Some attributes are special-cased, however:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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): ...
```

View file

@ -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: <bound method `name` of `C`>
# 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: <bound method `__hash__` of `MethodWrapperType`>
# 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`:

View file

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

View file

@ -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 `@<property_name>.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: <wrapper-descriptor `__get__` of `property` objects>
reveal_type(type(attr_property).__set__) # revealed: <wrapper-descriptor `__set__` of `property` objects>
```
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)
```

View file

@ -58,9 +58,8 @@ reveal_type(typing.__eq__) # revealed: <bound method `__eq__` of `ModuleType`>
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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Type<'db>>,
setter: Option<Type<'db>>,
}
/// 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<bool> 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.

View file

@ -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<usize>,
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()
}
),
);
}
}
}

View file

@ -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(_)

View file

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

View file

@ -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.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,
"<method-wrapper `__get__` of `{function}`>",
function = function.name(self.db)
)
}
Type::WrapperDescriptorDunderGet => {
f.write_str("<wrapper-descriptor `__get__` of `function` objects>")
Type::MethodWrapper(MethodWrapperKind::PropertyDunderGet(_)) => {
write!(f, "<method-wrapper `__get__` of `property` object>",)
}
Type::MethodWrapper(MethodWrapperKind::PropertyDunderSet(_)) => {
write!(f, "<method-wrapper `__set__` of `property` object>",)
}
Type::WrapperDescriptor(kind) => {
let (method, object) = match kind {
WrapperDescriptorKind::FunctionTypeDunderGet => ("__get__", "function"),
WrapperDescriptorKind::PropertyDunderGet => ("__get__", "property"),
WrapperDescriptorKind::PropertyDunderSet => ("__set__", "property"),
};
write!(f, "<wrapper-descriptor `{method}` of `{object}` objects>")
}
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)

View file

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

View file

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

View file

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