mirror of https://github.com/astral-sh/ruff
[red-knot] Add support for `@classmethod`s (#16305)
## Summary
Add support for `@classmethod`s.
```py
class C:
@classmethod
def f(cls, x: int) -> str:
return "a"
reveal_type(C.f(1)) # revealed: str
```
## Test Plan
New Markdown tests
This commit is contained in:
parent
81a57656d8
commit
141ba253da
|
|
@ -73,12 +73,12 @@ qux = (foo, bar)
|
||||||
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]
|
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]
|
||||||
|
|
||||||
# TODO: Infer "LiteralString"
|
# TODO: Infer "LiteralString"
|
||||||
reveal_type(foo.join(qux)) # revealed: @Todo(decorated method)
|
reveal_type(foo.join(qux)) # revealed: @Todo(overloaded method)
|
||||||
|
|
||||||
template: LiteralString = "{}, {}"
|
template: LiteralString = "{}, {}"
|
||||||
reveal_type(template) # revealed: Literal["{}, {}"]
|
reveal_type(template) # revealed: Literal["{}, {}"]
|
||||||
# TODO: Infer `LiteralString`
|
# TODO: Infer `LiteralString`
|
||||||
reveal_type(template.format(foo, bar)) # revealed: @Todo(decorated method)
|
reveal_type(template.format(foo, bar)) # revealed: @Todo(overloaded method)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Assignability
|
### Assignability
|
||||||
|
|
|
||||||
|
|
@ -1042,8 +1042,8 @@ Most attribute accesses on bool-literal types are delegated to `builtins.bool`,
|
||||||
bools are instances of that class:
|
bools are instances of that class:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
reveal_type(True.__and__) # revealed: @Todo(decorated method)
|
reveal_type(True.__and__) # revealed: @Todo(overloaded method)
|
||||||
reveal_type(False.__or__) # revealed: @Todo(decorated method)
|
reveal_type(False.__or__) # revealed: @Todo(overloaded method)
|
||||||
```
|
```
|
||||||
|
|
||||||
Some attributes are special-cased, however:
|
Some attributes are special-cased, however:
|
||||||
|
|
|
||||||
|
|
@ -306,7 +306,7 @@ reveal_type(1 + A()) # revealed: A
|
||||||
reveal_type(A() + "foo") # revealed: A
|
reveal_type(A() + "foo") # revealed: A
|
||||||
# TODO should be `A` since `str.__add__` doesn't support `A` instances
|
# TODO should be `A` since `str.__add__` doesn't support `A` instances
|
||||||
# TODO overloads
|
# TODO overloads
|
||||||
reveal_type("foo" + A()) # revealed: @Todo(return type)
|
reveal_type("foo" + A()) # revealed: @Todo(return type of decorated function)
|
||||||
|
|
||||||
reveal_type(A() + b"foo") # revealed: A
|
reveal_type(A() + b"foo") # revealed: A
|
||||||
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
|
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
|
||||||
|
|
@ -314,7 +314,7 @@ reveal_type(b"foo" + A()) # revealed: bytes
|
||||||
|
|
||||||
reveal_type(A() + ()) # revealed: A
|
reveal_type(A() + ()) # revealed: A
|
||||||
# TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances
|
# TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances
|
||||||
reveal_type(() + A()) # revealed: @Todo(return type)
|
reveal_type(() + A()) # revealed: @Todo(return type of decorated function)
|
||||||
|
|
||||||
literal_string_instance = "foo" * 1_000_000_000
|
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`:
|
# the test is not testing what it's meant to be testing if this isn't a `LiteralString`:
|
||||||
|
|
@ -323,7 +323,7 @@ reveal_type(literal_string_instance) # revealed: LiteralString
|
||||||
reveal_type(A() + literal_string_instance) # revealed: A
|
reveal_type(A() + literal_string_instance) # revealed: A
|
||||||
# TODO should be `A` since `str.__add__` doesn't support `A` instances
|
# TODO should be `A` since `str.__add__` doesn't support `A` instances
|
||||||
# TODO overloads
|
# TODO overloads
|
||||||
reveal_type(literal_string_instance + A()) # revealed: @Todo(return type)
|
reveal_type(literal_string_instance + A()) # revealed: @Todo(return type of decorated function)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Operations involving instances of classes inheriting from `Any`
|
## Operations involving instances of classes inheriting from `Any`
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,9 @@ reveal_type(1 ** (largest_u32 + 1)) # revealed: int
|
||||||
reveal_type(2**largest_u32) # revealed: int
|
reveal_type(2**largest_u32) # revealed: int
|
||||||
|
|
||||||
def variable(x: int):
|
def variable(x: int):
|
||||||
reveal_type(x**2) # revealed: @Todo(return type)
|
reveal_type(x**2) # revealed: @Todo(return type of decorated function)
|
||||||
reveal_type(2**x) # revealed: @Todo(return type)
|
reveal_type(2**x) # revealed: @Todo(return type of decorated function)
|
||||||
reveal_type(x**x) # revealed: @Todo(return type)
|
reveal_type(x**x) # revealed: @Todo(return type of decorated function)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Division by Zero
|
## Division by Zero
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ def bar() -> str:
|
||||||
return "bar"
|
return "bar"
|
||||||
|
|
||||||
# TODO: should reveal `int`, as the decorator replaces `bar` with `foo`
|
# TODO: should reveal `int`, as the decorator replaces `bar` with `foo`
|
||||||
reveal_type(bar()) # revealed: @Todo(return type)
|
reveal_type(bar()) # revealed: @Todo(return type of decorated function)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Invalid callable
|
## Invalid callable
|
||||||
|
|
|
||||||
|
|
@ -255,4 +255,126 @@ method_wrapper()
|
||||||
method_wrapper(C(), C, "one too many")
|
method_wrapper(C(), C, "one too many")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `@classmethod`
|
||||||
|
|
||||||
|
### Basic
|
||||||
|
|
||||||
|
When a `@classmethod` attribute is accessed, it returns a bound method object, even when accessed on
|
||||||
|
the class object itself:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
class C:
|
||||||
|
@classmethod
|
||||||
|
def f(cls: type[C], x: int) -> str:
|
||||||
|
return "a"
|
||||||
|
|
||||||
|
reveal_type(C.f) # revealed: <bound method `f` of `Literal[C]`>
|
||||||
|
reveal_type(C().f) # revealed: <bound method `f` of `type[C]`>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `cls` method argument is then implicitly passed as the first argument when calling the method:
|
||||||
|
|
||||||
|
```py
|
||||||
|
reveal_type(C.f(1)) # revealed: str
|
||||||
|
reveal_type(C().f(1)) # revealed: str
|
||||||
|
```
|
||||||
|
|
||||||
|
When the class method is called incorrectly, we detect it:
|
||||||
|
|
||||||
|
```py
|
||||||
|
C.f("incorrect") # error: [invalid-argument-type]
|
||||||
|
C.f() # error: [missing-argument]
|
||||||
|
C.f(1, 2) # error: [too-many-positional-arguments]
|
||||||
|
```
|
||||||
|
|
||||||
|
If the `cls` parameter is wrongly annotated, we emit an error at the call site:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class D:
|
||||||
|
@classmethod
|
||||||
|
def f(cls: D):
|
||||||
|
# This function is wrongly annotated, it should be `type[D]` instead of `D`
|
||||||
|
pass
|
||||||
|
|
||||||
|
# error: [invalid-argument-type] "Object of type `Literal[D]` cannot be assigned to parameter 1 (`cls`); expected type `D`"
|
||||||
|
D.f()
|
||||||
|
```
|
||||||
|
|
||||||
|
When a class method is accessed on a derived class, it is bound to that derived class:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Derived(C):
|
||||||
|
pass
|
||||||
|
|
||||||
|
reveal_type(Derived.f) # revealed: <bound method `f` of `Literal[Derived]`>
|
||||||
|
reveal_type(Derived().f) # revealed: <bound method `f` of `type[Derived]`>
|
||||||
|
|
||||||
|
reveal_type(Derived.f(1)) # revealed: str
|
||||||
|
reveal_type(Derived().f(1)) # revealed: str
|
||||||
|
```
|
||||||
|
|
||||||
|
### Accessing the classmethod as a static member
|
||||||
|
|
||||||
|
Accessing a `@classmethod`-decorated function at runtime returns a `classmethod` object. We
|
||||||
|
currently don't model this explicitly:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from inspect import getattr_static
|
||||||
|
|
||||||
|
class C:
|
||||||
|
@classmethod
|
||||||
|
def f(cls): ...
|
||||||
|
|
||||||
|
reveal_type(getattr_static(C, "f")) # revealed: Literal[f]
|
||||||
|
reveal_type(getattr_static(C, "f").__get__) # revealed: <method-wrapper `__get__` of `f`>
|
||||||
|
```
|
||||||
|
|
||||||
|
But we correctly model how the `classmethod` descriptor works:
|
||||||
|
|
||||||
|
```py
|
||||||
|
reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: <bound method `f` of `Literal[C]`>
|
||||||
|
reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: <bound method `f` of `Literal[C]`>
|
||||||
|
reveal_type(getattr_static(C, "f").__get__(C())) # revealed: <bound method `f` of `type[C]`>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `owner` argument takes precedence over the `instance` argument:
|
||||||
|
|
||||||
|
```py
|
||||||
|
reveal_type(getattr_static(C, "f").__get__("dummy", C)) # revealed: <bound method `f` of `Literal[C]`>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Classmethods mixed with other decorators
|
||||||
|
|
||||||
|
When a `@classmethod` is additionally decorated with another decorator, it is still treated as a
|
||||||
|
class method:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
def does_nothing[T](f: T) -> T:
|
||||||
|
return f
|
||||||
|
|
||||||
|
class C:
|
||||||
|
@classmethod
|
||||||
|
@does_nothing
|
||||||
|
def f1(cls: type[C], x: int) -> str:
|
||||||
|
return "a"
|
||||||
|
|
||||||
|
@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`:
|
||||||
|
|
||||||
|
reveal_type(C.f1(1)) # revealed: @Todo(return type of decorated function)
|
||||||
|
reveal_type(C().f1(1)) # revealed: @Todo(decorated method)
|
||||||
|
|
||||||
|
reveal_type(C.f2(1)) # revealed: @Todo(return type of decorated function)
|
||||||
|
reveal_type(C().f2(1)) # revealed: @Todo(decorated method)
|
||||||
|
```
|
||||||
|
|
||||||
[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods
|
[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods
|
||||||
|
|
|
||||||
|
|
@ -201,14 +201,11 @@ class C:
|
||||||
|
|
||||||
c1 = C.factory("test") # okay
|
c1 = C.factory("test") # okay
|
||||||
|
|
||||||
# TODO: should be `C`
|
reveal_type(c1) # revealed: C
|
||||||
reveal_type(c1) # revealed: @Todo(return type)
|
|
||||||
|
|
||||||
# TODO: should be `str`
|
reveal_type(C.get_name()) # revealed: str
|
||||||
reveal_type(C.get_name()) # revealed: @Todo(return type)
|
|
||||||
|
|
||||||
# TODO: should be `str`
|
reveal_type(C("42").get_name()) # revealed: str
|
||||||
reveal_type(C("42").get_name()) # revealed: @Todo(decorated method)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Descriptors only work when used as class variables
|
## Descriptors only work when used as class variables
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,7 @@ class A:
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
__slots__ += ("a", "b")
|
__slots__ += ("a", "b")
|
||||||
|
|
||||||
reveal_type(A.__slots__) # revealed: @Todo(return type)
|
reveal_type(A.__slots__) # revealed: @Todo(return type of decorated function)
|
||||||
|
|
||||||
class B:
|
class B:
|
||||||
__slots__ = ("c", "d")
|
__slots__ = ("c", "d")
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ reveal_type(y) # revealed: Unknown
|
||||||
def _(n: int):
|
def _(n: int):
|
||||||
a = b"abcde"[n]
|
a = b"abcde"[n]
|
||||||
# TODO: Support overloads... Should be `bytes`
|
# TODO: Support overloads... Should be `bytes`
|
||||||
reveal_type(a) # revealed: @Todo(return type)
|
reveal_type(a) # revealed: @Todo(return type of decorated function)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Slices
|
## Slices
|
||||||
|
|
@ -44,10 +44,10 @@ b[::0] # error: [zero-stepsize-in-slice]
|
||||||
def _(m: int, n: int):
|
def _(m: int, n: int):
|
||||||
byte_slice1 = b[m:n]
|
byte_slice1 = b[m:n]
|
||||||
# TODO: Support overloads... Should be `bytes`
|
# TODO: Support overloads... Should be `bytes`
|
||||||
reveal_type(byte_slice1) # revealed: @Todo(return type)
|
reveal_type(byte_slice1) # revealed: @Todo(return type of decorated function)
|
||||||
|
|
||||||
def _(s: bytes) -> bytes:
|
def _(s: bytes) -> bytes:
|
||||||
byte_slice2 = s[0:5]
|
byte_slice2 = s[0:5]
|
||||||
# TODO: Support overloads... Should be `bytes`
|
# TODO: Support overloads... Should be `bytes`
|
||||||
reveal_type(byte_slice2) # revealed: @Todo(return type)
|
reveal_type(byte_slice2) # revealed: @Todo(return type of decorated function)
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,13 @@ x = [1, 2, 3]
|
||||||
reveal_type(x) # revealed: list
|
reveal_type(x) # revealed: list
|
||||||
|
|
||||||
# TODO reveal int
|
# TODO reveal int
|
||||||
reveal_type(x[0]) # revealed: @Todo(return type)
|
reveal_type(x[0]) # revealed: @Todo(return type of decorated function)
|
||||||
|
|
||||||
# TODO reveal list
|
# TODO reveal list
|
||||||
reveal_type(x[0:1]) # revealed: @Todo(return type)
|
reveal_type(x[0:1]) # revealed: @Todo(return type of decorated function)
|
||||||
|
|
||||||
# TODO error
|
# TODO error
|
||||||
reveal_type(x["a"]) # revealed: @Todo(return type)
|
reveal_type(x["a"]) # revealed: @Todo(return type of decorated function)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Assignments within list assignment
|
## Assignments within list assignment
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ reveal_type(b) # revealed: Unknown
|
||||||
def _(n: int):
|
def _(n: int):
|
||||||
a = "abcde"[n]
|
a = "abcde"[n]
|
||||||
# TODO: Support overloads... Should be `str`
|
# TODO: Support overloads... Should be `str`
|
||||||
reveal_type(a) # revealed: @Todo(return type)
|
reveal_type(a) # revealed: @Todo(return type of decorated function)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Slices
|
## Slices
|
||||||
|
|
@ -76,11 +76,11 @@ def _(m: int, n: int, s2: str):
|
||||||
|
|
||||||
substring1 = s[m:n]
|
substring1 = s[m:n]
|
||||||
# TODO: Support overloads... Should be `LiteralString`
|
# TODO: Support overloads... Should be `LiteralString`
|
||||||
reveal_type(substring1) # revealed: @Todo(return type)
|
reveal_type(substring1) # revealed: @Todo(return type of decorated function)
|
||||||
|
|
||||||
substring2 = s2[0:5]
|
substring2 = s2[0:5]
|
||||||
# TODO: Support overloads... Should be `str`
|
# TODO: Support overloads... Should be `str`
|
||||||
reveal_type(substring2) # revealed: @Todo(return type)
|
reveal_type(substring2) # revealed: @Todo(return type of decorated function)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Unsupported slice types
|
## Unsupported slice types
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ def _(m: int, n: int):
|
||||||
|
|
||||||
tuple_slice = t[m:n]
|
tuple_slice = t[m:n]
|
||||||
# TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]`
|
# TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]`
|
||||||
reveal_type(tuple_slice) # revealed: @Todo(return type)
|
reveal_type(tuple_slice) # revealed: @Todo(return type of decorated function)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Inheritance
|
## Inheritance
|
||||||
|
|
|
||||||
|
|
@ -1948,13 +1948,29 @@ impl<'db> Type<'db> {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
|
if function.has_known_class_decorator(db, KnownClass::Classmethod)
|
||||||
|
&& function.decorators(db).len() == 1
|
||||||
|
{
|
||||||
|
if let Some(owner) = arguments.second_argument() {
|
||||||
|
Some(Type::Callable(CallableType::BoundMethod(
|
||||||
|
BoundMethodType::new(db, function, owner),
|
||||||
|
)))
|
||||||
|
} else if let Some(instance) = arguments.first_argument() {
|
||||||
|
Some(Type::Callable(CallableType::BoundMethod(
|
||||||
|
BoundMethodType::new(db, function, instance.to_meta_type(db)),
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Some(Type::unknown())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
Some(match arguments.first_argument() {
|
Some(match arguments.first_argument() {
|
||||||
Some(ty) if ty.is_none(db) => Type::FunctionLiteral(function),
|
Some(ty) if ty.is_none(db) => Type::FunctionLiteral(function),
|
||||||
Some(instance) => Type::Callable(CallableType::BoundMethod(
|
Some(instance) => Type::Callable(CallableType::BoundMethod(
|
||||||
BoundMethodType::new(db, function, instance),
|
BoundMethodType::new(db, function, instance),
|
||||||
)),
|
)),
|
||||||
_ => Type::unknown(),
|
_ => Type::unknown(),
|
||||||
}),
|
})
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let binding = bind_call(db, arguments, &signature, self);
|
let binding = bind_call(db, arguments, &signature, self);
|
||||||
|
|
@ -2004,18 +2020,40 @@ impl<'db> Type<'db> {
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
Some(
|
Some(
|
||||||
match (arguments.first_argument(), arguments.second_argument()) {
|
if let Some(function_ty @ Type::FunctionLiteral(function)) =
|
||||||
(Some(function @ Type::FunctionLiteral(_)), Some(instance))
|
arguments.first_argument()
|
||||||
if instance.is_none(db) =>
|
|
||||||
{
|
{
|
||||||
function
|
if function.has_known_class_decorator(db, KnownClass::Classmethod)
|
||||||
}
|
&& function.decorators(db).len() == 1
|
||||||
(Some(Type::FunctionLiteral(function)), Some(instance)) => {
|
{
|
||||||
|
if let Some(owner) = arguments.third_argument() {
|
||||||
Type::Callable(CallableType::BoundMethod(BoundMethodType::new(
|
Type::Callable(CallableType::BoundMethod(BoundMethodType::new(
|
||||||
db, function, instance,
|
db, function, owner,
|
||||||
)))
|
)))
|
||||||
|
} else if let Some(instance) = arguments.second_argument() {
|
||||||
|
Type::Callable(CallableType::BoundMethod(BoundMethodType::new(
|
||||||
|
db,
|
||||||
|
function,
|
||||||
|
instance.to_meta_type(db),
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Type::unknown()
|
||||||
}
|
}
|
||||||
_ => Type::unknown(),
|
} else {
|
||||||
|
if let Some(instance) = arguments.second_argument() {
|
||||||
|
if instance.is_none(db) {
|
||||||
|
function_ty
|
||||||
|
} else {
|
||||||
|
Type::Callable(CallableType::BoundMethod(
|
||||||
|
BoundMethodType::new(db, function, instance),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Type::unknown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Type::unknown()
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -2108,6 +2146,10 @@ impl<'db> Type<'db> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some(KnownFunction::Overload) => {
|
||||||
|
binding.set_return_type(todo_type!("overload(..) return type"));
|
||||||
|
}
|
||||||
|
|
||||||
Some(KnownFunction::GetattrStatic) => {
|
Some(KnownFunction::GetattrStatic) => {
|
||||||
let Some((instance_ty, attr_name, default)) =
|
let Some((instance_ty, attr_name, default)) =
|
||||||
binding.three_parameter_types()
|
binding.three_parameter_types()
|
||||||
|
|
@ -2853,6 +2895,7 @@ pub enum KnownClass {
|
||||||
Property,
|
Property,
|
||||||
BaseException,
|
BaseException,
|
||||||
BaseExceptionGroup,
|
BaseExceptionGroup,
|
||||||
|
Classmethod,
|
||||||
// Types
|
// Types
|
||||||
GenericAlias,
|
GenericAlias,
|
||||||
ModuleType,
|
ModuleType,
|
||||||
|
|
@ -2937,7 +2980,8 @@ impl<'db> KnownClass {
|
||||||
| Self::Counter
|
| Self::Counter
|
||||||
| Self::DefaultDict
|
| Self::DefaultDict
|
||||||
| Self::Deque
|
| Self::Deque
|
||||||
| Self::Float => Truthiness::Ambiguous,
|
| Self::Float
|
||||||
|
| Self::Classmethod => Truthiness::Ambiguous,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2961,6 +3005,7 @@ impl<'db> KnownClass {
|
||||||
Self::Property => "property",
|
Self::Property => "property",
|
||||||
Self::BaseException => "BaseException",
|
Self::BaseException => "BaseException",
|
||||||
Self::BaseExceptionGroup => "BaseExceptionGroup",
|
Self::BaseExceptionGroup => "BaseExceptionGroup",
|
||||||
|
Self::Classmethod => "classmethod",
|
||||||
Self::GenericAlias => "GenericAlias",
|
Self::GenericAlias => "GenericAlias",
|
||||||
Self::ModuleType => "ModuleType",
|
Self::ModuleType => "ModuleType",
|
||||||
Self::FunctionType => "FunctionType",
|
Self::FunctionType => "FunctionType",
|
||||||
|
|
@ -3042,6 +3087,7 @@ impl<'db> KnownClass {
|
||||||
| Self::Dict
|
| Self::Dict
|
||||||
| Self::BaseException
|
| Self::BaseException
|
||||||
| Self::BaseExceptionGroup
|
| Self::BaseExceptionGroup
|
||||||
|
| Self::Classmethod
|
||||||
| Self::Slice
|
| Self::Slice
|
||||||
| Self::Range
|
| Self::Range
|
||||||
| Self::Property => KnownModule::Builtins,
|
| Self::Property => KnownModule::Builtins,
|
||||||
|
|
@ -3114,6 +3160,7 @@ impl<'db> KnownClass {
|
||||||
| KnownClass::Property
|
| KnownClass::Property
|
||||||
| KnownClass::BaseException
|
| KnownClass::BaseException
|
||||||
| KnownClass::BaseExceptionGroup
|
| KnownClass::BaseExceptionGroup
|
||||||
|
| KnownClass::Classmethod
|
||||||
| KnownClass::GenericAlias
|
| KnownClass::GenericAlias
|
||||||
| KnownClass::ModuleType
|
| KnownClass::ModuleType
|
||||||
| KnownClass::FunctionType
|
| KnownClass::FunctionType
|
||||||
|
|
@ -3176,6 +3223,7 @@ impl<'db> KnownClass {
|
||||||
| Self::SupportsIndex
|
| Self::SupportsIndex
|
||||||
| Self::BaseException
|
| Self::BaseException
|
||||||
| Self::BaseExceptionGroup
|
| Self::BaseExceptionGroup
|
||||||
|
| Self::Classmethod
|
||||||
| Self::TypeVar => false,
|
| Self::TypeVar => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3200,8 +3248,10 @@ impl<'db> KnownClass {
|
||||||
"list" => Self::List,
|
"list" => Self::List,
|
||||||
"slice" => Self::Slice,
|
"slice" => Self::Slice,
|
||||||
"range" => Self::Range,
|
"range" => Self::Range,
|
||||||
|
"property" => Self::Property,
|
||||||
"BaseException" => Self::BaseException,
|
"BaseException" => Self::BaseException,
|
||||||
"BaseExceptionGroup" => Self::BaseExceptionGroup,
|
"BaseExceptionGroup" => Self::BaseExceptionGroup,
|
||||||
|
"classmethod" => Self::Classmethod,
|
||||||
"GenericAlias" => Self::GenericAlias,
|
"GenericAlias" => Self::GenericAlias,
|
||||||
"NoneType" => Self::NoneType,
|
"NoneType" => Self::NoneType,
|
||||||
"ModuleType" => Self::ModuleType,
|
"ModuleType" => Self::ModuleType,
|
||||||
|
|
@ -3266,6 +3316,7 @@ impl<'db> KnownClass {
|
||||||
| Self::BaseException
|
| Self::BaseException
|
||||||
| Self::EllipsisType
|
| Self::EllipsisType
|
||||||
| Self::BaseExceptionGroup
|
| Self::BaseExceptionGroup
|
||||||
|
| Self::Classmethod
|
||||||
| Self::FunctionType
|
| Self::FunctionType
|
||||||
| Self::MethodType
|
| Self::MethodType
|
||||||
| Self::MethodWrapperType
|
| Self::MethodWrapperType
|
||||||
|
|
@ -3919,8 +3970,18 @@ pub struct FunctionType<'db> {
|
||||||
|
|
||||||
#[salsa::tracked]
|
#[salsa::tracked]
|
||||||
impl<'db> FunctionType<'db> {
|
impl<'db> FunctionType<'db> {
|
||||||
pub fn has_decorator(self, db: &dyn Db, decorator: Type<'_>) -> bool {
|
pub fn has_known_class_decorator(self, db: &dyn Db, decorator: KnownClass) -> bool {
|
||||||
self.decorators(db).contains(&decorator)
|
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))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Typed externally-visible signature for this function.
|
/// Typed externally-visible signature for this function.
|
||||||
|
|
@ -3937,13 +3998,23 @@ impl<'db> FunctionType<'db> {
|
||||||
/// would depend on the function's AST and rerun for every change in that file.
|
/// would depend on the function's AST and rerun for every change in that file.
|
||||||
#[salsa::tracked(return_ref)]
|
#[salsa::tracked(return_ref)]
|
||||||
pub fn signature(self, db: &'db dyn Db) -> Signature<'db> {
|
pub fn signature(self, db: &'db dyn Db) -> Signature<'db> {
|
||||||
let function_stmt_node = self.body_scope(db).node(db).expect_function();
|
|
||||||
let internal_signature = self.internal_signature(db);
|
let internal_signature = self.internal_signature(db);
|
||||||
if function_stmt_node.decorator_list.is_empty() {
|
|
||||||
return internal_signature;
|
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")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
internal_signature
|
||||||
}
|
}
|
||||||
// TODO process the effect of decorators on the signature
|
|
||||||
Signature::todo()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Typed internally-visible signature for this function.
|
/// Typed internally-visible signature for this function.
|
||||||
|
|
@ -3989,6 +4060,8 @@ pub enum KnownFunction {
|
||||||
AssertType,
|
AssertType,
|
||||||
/// `typing(_extensions).cast`
|
/// `typing(_extensions).cast`
|
||||||
Cast,
|
Cast,
|
||||||
|
/// `typing(_extensions).overload`
|
||||||
|
Overload,
|
||||||
|
|
||||||
/// `inspect.getattr_static`
|
/// `inspect.getattr_static`
|
||||||
GetattrStatic,
|
GetattrStatic,
|
||||||
|
|
@ -4036,6 +4109,7 @@ impl KnownFunction {
|
||||||
"no_type_check" => Self::NoTypeCheck,
|
"no_type_check" => Self::NoTypeCheck,
|
||||||
"assert_type" => Self::AssertType,
|
"assert_type" => Self::AssertType,
|
||||||
"cast" => Self::Cast,
|
"cast" => Self::Cast,
|
||||||
|
"overload" => Self::Overload,
|
||||||
"getattr_static" => Self::GetattrStatic,
|
"getattr_static" => Self::GetattrStatic,
|
||||||
"static_assert" => Self::StaticAssert,
|
"static_assert" => Self::StaticAssert,
|
||||||
"is_subtype_of" => Self::IsSubtypeOf,
|
"is_subtype_of" => Self::IsSubtypeOf,
|
||||||
|
|
@ -4063,7 +4137,12 @@ impl KnownFunction {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Self::Len | Self::Repr => module.is_builtins(),
|
Self::Len | Self::Repr => module.is_builtins(),
|
||||||
Self::AssertType | Self::Cast | Self::RevealType | Self::Final | Self::NoTypeCheck => {
|
Self::AssertType
|
||||||
|
| Self::Cast
|
||||||
|
| Self::Overload
|
||||||
|
| Self::RevealType
|
||||||
|
| Self::Final
|
||||||
|
| Self::NoTypeCheck => {
|
||||||
matches!(module, KnownModule::Typing | KnownModule::TypingExtensions)
|
matches!(module, KnownModule::Typing | KnownModule::TypingExtensions)
|
||||||
}
|
}
|
||||||
Self::GetattrStatic => {
|
Self::GetattrStatic => {
|
||||||
|
|
@ -4100,6 +4179,7 @@ impl KnownFunction {
|
||||||
Self::ConstraintFunction(_)
|
Self::ConstraintFunction(_)
|
||||||
| Self::Len
|
| Self::Len
|
||||||
| Self::Repr
|
| Self::Repr
|
||||||
|
| Self::Overload
|
||||||
| Self::Final
|
| Self::Final
|
||||||
| Self::NoTypeCheck
|
| Self::NoTypeCheck
|
||||||
| Self::RevealType
|
| Self::RevealType
|
||||||
|
|
@ -4767,10 +4847,17 @@ impl<'db> Class<'db> {
|
||||||
|
|
||||||
if let Some(function) = declared_ty.into_function_literal() {
|
if let Some(function) = declared_ty.into_function_literal() {
|
||||||
// TODO: Eventually, we are going to process all decorators correctly. This is
|
// TODO: Eventually, we are going to process all decorators correctly. This is
|
||||||
// just a temporary heuristic to provide a broad categorization into properties
|
// just a temporary heuristic to provide a broad categorization
|
||||||
// and non-property methods.
|
|
||||||
if function.has_decorator(db, KnownClass::Property.to_class_literal(db)) {
|
if function.has_known_class_decorator(db, KnownClass::Classmethod)
|
||||||
|
&& function.decorators(db).len() == 1
|
||||||
|
{
|
||||||
|
SymbolAndQualifiers(Symbol::bound(declared_ty), qualifiers)
|
||||||
|
} else if function.has_known_class_decorator(db, KnownClass::Property) {
|
||||||
SymbolAndQualifiers::todo("@property")
|
SymbolAndQualifiers::todo("@property")
|
||||||
|
} else if function.has_known_function_decorator(db, KnownFunction::Overload)
|
||||||
|
{
|
||||||
|
SymbolAndQualifiers::todo("overloaded method")
|
||||||
} else if !function.decorators(db).is_empty() {
|
} else if !function.decorators(db).is_empty() {
|
||||||
SymbolAndQualifiers::todo("decorated method")
|
SymbolAndQualifiers::todo("decorated method")
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,11 @@ impl<'a, 'db> CallArguments<'a, 'db> {
|
||||||
pub(crate) fn second_argument(&self) -> Option<Type<'db>> {
|
pub(crate) fn second_argument(&self) -> Option<Type<'db>> {
|
||||||
self.0.get(1).map(Argument::ty)
|
self.0.get(1).map(Argument::ty)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO this should be eliminated in favor of [`bind_call`]
|
||||||
|
pub(crate) fn third_argument(&self) -> Option<Type<'db>> {
|
||||||
|
self.0.get(2).map(Argument::ty)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'db, 'a, 'b> IntoIterator for &'b CallArguments<'a, 'db> {
|
impl<'db, 'a, 'b> IntoIterator for &'b CallArguments<'a, 'db> {
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,11 @@ impl<'db> Signature<'db> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a todo signature: (*args: Todo, **kwargs: Todo) -> Todo
|
/// Return a todo signature: (*args: Todo, **kwargs: Todo) -> Todo
|
||||||
pub(crate) fn todo() -> Self {
|
#[allow(unused_variables)] // 'reason' only unused in debug builds
|
||||||
|
pub(crate) fn todo(reason: &'static str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
parameters: Parameters::todo(),
|
parameters: Parameters::todo(),
|
||||||
return_ty: Some(todo_type!("return type")),
|
return_ty: Some(todo_type!(reason)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -650,7 +651,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let func = get_function_f(&db, "/src/a.py");
|
let func = get_function_f(&db, "/src/a.py");
|
||||||
|
|
||||||
let expected_sig = Signature::todo();
|
let expected_sig = Signature::todo("return type of decorated function");
|
||||||
|
|
||||||
// With no decorators, internal and external signature are the same
|
// With no decorators, internal and external signature are the same
|
||||||
assert_eq!(func.signature(&db), &expected_sig);
|
assert_eq!(func.signature(&db), &expected_sig);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue