mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-24 05:26:23 +00:00
[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:
parent
e1b5b0de71
commit
ae2cf91a36
26 changed files with 1201 additions and 261 deletions
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`:
|
||||
|
|
|
@ -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
|
||||
|
|
237
crates/red_knot_python_semantic/resources/mdtest/decorators.md
Normal file
237
crates/red_knot_python_semantic/resources/mdtest/decorators.md
Normal 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): ...
|
||||
```
|
|
@ -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`:
|
||||
|
|
|
@ -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
|
||||
|
|
305
crates/red_knot_python_semantic/resources/mdtest/properties.md
Normal file
305
crates/red_knot_python_semantic/resources/mdtest/properties.md
Normal 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)
|
||||
```
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(_)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue