mirror of
https://github.com/astral-sh/ruff.git
synced 2025-12-23 09:19:39 +00:00
[ty] Fix @staticmethod combined with other decorators incorrectly binding self
This commit is contained in:
parent
ad41728204
commit
50fd3e213d
9 changed files with 104 additions and 12 deletions
|
|
@ -194,7 +194,7 @@ static SYMPY: Benchmark = Benchmark::new(
|
|||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY312,
|
||||
},
|
||||
13100,
|
||||
13106,
|
||||
);
|
||||
|
||||
static TANJUN: Benchmark = Benchmark::new(
|
||||
|
|
|
|||
|
|
@ -588,6 +588,31 @@ reveal_type(C.f2(1)) # revealed: str
|
|||
reveal_type(C().f2(1)) # revealed: str
|
||||
```
|
||||
|
||||
When a `@staticmethod` is decorated with `@contextmanager`, accessing it from an instance should not
|
||||
bind `self`:
|
||||
|
||||
```py
|
||||
from contextlib import contextmanager
|
||||
from collections.abc import Iterator
|
||||
|
||||
class D:
|
||||
@staticmethod
|
||||
@contextmanager
|
||||
def ctx(num: int) -> Iterator[int]:
|
||||
yield num
|
||||
|
||||
def use_ctx(self) -> None:
|
||||
# Accessing via self should not bind self
|
||||
with self.ctx(10) as x:
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
# Accessing via class works
|
||||
reveal_type(D.ctx(5)) # revealed: _GeneratorContextManager[int, None, None]
|
||||
|
||||
# Accessing via instance should also work (no self-binding)
|
||||
reveal_type(D().ctx(5)) # revealed: _GeneratorContextManager[int, None, None]
|
||||
```
|
||||
|
||||
### `__new__`
|
||||
|
||||
`__new__` is an implicit `@staticmethod`; accessing it on an instance does not bind the `cls`
|
||||
|
|
|
|||
|
|
@ -119,8 +119,8 @@ class OtherChild(Parent): ...
|
|||
|
||||
class Grandchild(OtherChild):
|
||||
@staticmethod
|
||||
# TODO: we should emit a Liskov violation here too
|
||||
# error: [override-of-final-method]
|
||||
# error: [invalid-method-override]
|
||||
def foo(): ...
|
||||
@property
|
||||
# TODO: we should emit a Liskov violation here too
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ class LiskovViolatingButNotOverrideViolating(Parent):
|
|||
|
||||
@staticmethod
|
||||
@override
|
||||
def class_method1() -> int: ...
|
||||
def class_method1() -> int: ... # error: [invalid-method-override]
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -96,8 +96,8 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md
|
|||
82 |
|
||||
83 | class Grandchild(OtherChild):
|
||||
84 | @staticmethod
|
||||
85 | # TODO: we should emit a Liskov violation here too
|
||||
86 | # error: [override-of-final-method]
|
||||
85 | # error: [override-of-final-method]
|
||||
86 | # error: [invalid-method-override]
|
||||
87 | def foo(): ...
|
||||
88 | @property
|
||||
89 | # TODO: we should emit a Liskov violation here too
|
||||
|
|
@ -406,12 +406,38 @@ note: This is an unsafe fix and may change runtime behavior
|
|||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `foo`
|
||||
--> src/mdtest_snippet.pyi:87:9
|
||||
|
|
||||
85 | # error: [override-of-final-method]
|
||||
86 | # error: [invalid-method-override]
|
||||
87 | def foo(): ...
|
||||
| ^^^^^ Definition is incompatible with `Parent.foo`
|
||||
88 | @property
|
||||
89 | # TODO: we should emit a Liskov violation here too
|
||||
|
|
||||
::: src/mdtest_snippet.pyi:7:9
|
||||
|
|
||||
5 | class Parent:
|
||||
6 | @final
|
||||
7 | def foo(self): ...
|
||||
| --------- `Parent.foo` defined here
|
||||
8 |
|
||||
9 | @final
|
||||
|
|
||||
info: `Grandchild.foo` is a staticmethod but `Parent.foo` is an instance method
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `Parent.foo`
|
||||
--> src/mdtest_snippet.pyi:87:9
|
||||
|
|
||||
85 | # TODO: we should emit a Liskov violation here too
|
||||
86 | # error: [override-of-final-method]
|
||||
85 | # error: [override-of-final-method]
|
||||
86 | # error: [invalid-method-override]
|
||||
87 | def foo(): ...
|
||||
| ^^^ Overrides a definition from superclass `Parent`
|
||||
88 | @property
|
||||
|
|
@ -434,8 +460,8 @@ info: rule `override-of-final-method` is enabled by default
|
|||
82 |
|
||||
83 | class Grandchild(OtherChild):
|
||||
- @staticmethod
|
||||
- # TODO: we should emit a Liskov violation here too
|
||||
- # error: [override-of-final-method]
|
||||
- # error: [invalid-method-override]
|
||||
- def foo(): ...
|
||||
84 +
|
||||
85 | @property
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/override.md
|
|||
174 |
|
||||
175 | @staticmethod
|
||||
176 | @override
|
||||
177 | def class_method1() -> int: ...
|
||||
177 | def class_method1() -> int: ... # error: [invalid-method-override]
|
||||
178 |
|
||||
179 | @classmethod
|
||||
180 | @override
|
||||
|
|
@ -367,6 +367,31 @@ info: rule `invalid-explicit-override` is enabled by default
|
|||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `class_method1`
|
||||
--> src/mdtest_snippet.pyi:177:9
|
||||
|
|
||||
175 | @staticmethod
|
||||
176 | @override
|
||||
177 | def class_method1() -> int: ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.class_method1`
|
||||
178 |
|
||||
179 | @classmethod
|
||||
|
|
||||
::: src/mdtest_snippet.pyi:21:9
|
||||
|
|
||||
20 | @classmethod
|
||||
21 | def class_method1(cls) -> int: ...
|
||||
| ------------------------- `Parent.class_method1` defined here
|
||||
22 |
|
||||
23 | @staticmethod
|
||||
|
|
||||
info: `LiskovViolatingButNotOverrideViolating.class_method1` is a staticmethod but `Parent.class_method1` is a classmethod
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-explicit-override]: Method `bar` is decorated with `@override` but does not override anything
|
||||
--> src/mdtest_snippet.pyi:209:9
|
||||
|
|
|
|||
|
|
@ -4685,6 +4685,11 @@ impl<'db> Type<'db> {
|
|||
owner.display(db)
|
||||
);
|
||||
match self {
|
||||
Type::Callable(callable) if callable.is_staticmethod_like(db) => {
|
||||
// For "staticmethod-like" callables, model the behavior of `staticmethod.__get__`.
|
||||
// The underlying function is returned as-is, without binding self.
|
||||
return Some((self, AttributeKind::NormalOrNonDataDescriptor));
|
||||
}
|
||||
Type::Callable(callable)
|
||||
if callable.is_function_like(db) || callable.is_classmethod_like(db) =>
|
||||
{
|
||||
|
|
@ -12389,6 +12394,10 @@ pub enum CallableTypeKind {
|
|||
/// instances, i.e. they bind `self`.
|
||||
FunctionLike,
|
||||
|
||||
/// A callable type that represents a staticmethod. These callables do not bind `self`
|
||||
/// when accessed as attributes on instances - they return the underlying function as-is.
|
||||
StaticMethodLike,
|
||||
|
||||
/// A callable type that we believe represents a classmethod (i.e. it will unconditionally bind
|
||||
/// the first argument on `__get__`).
|
||||
ClassMethodLike,
|
||||
|
|
@ -12493,6 +12502,10 @@ impl<'db> CallableType<'db> {
|
|||
matches!(self.kind(db), CallableTypeKind::ClassMethodLike)
|
||||
}
|
||||
|
||||
pub(crate) fn is_staticmethod_like(self, db: &'db dyn Db) -> bool {
|
||||
matches!(self.kind(db), CallableTypeKind::StaticMethodLike)
|
||||
}
|
||||
|
||||
pub(crate) fn bind_self(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
|
|
|
|||
|
|
@ -1089,6 +1089,8 @@ impl<'db> FunctionType<'db> {
|
|||
pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> {
|
||||
let kind = if self.is_classmethod(db) {
|
||||
CallableTypeKind::ClassMethodLike
|
||||
} else if self.is_staticmethod(db) {
|
||||
CallableTypeKind::StaticMethodLike
|
||||
} else {
|
||||
CallableTypeKind::FunctionLike
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2436,6 +2436,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
.and_then(CallableTypes::exactly_one)
|
||||
.and_then(|callable| match callable.kind(self.db()) {
|
||||
kind @ (CallableTypeKind::FunctionLike
|
||||
| CallableTypeKind::StaticMethodLike
|
||||
| CallableTypeKind::ClassMethodLike) => Some(kind),
|
||||
_ => None,
|
||||
});
|
||||
|
|
@ -2445,9 +2446,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
{
|
||||
// When a method on a class is decorated with a function that returns a
|
||||
// `Callable`, assume that the returned callable is also function-like (or
|
||||
// classmethod-like). See "Decorating a method with a `Callable`-typed
|
||||
// decorator" in `callables_as_descriptors.md` for the extended
|
||||
// explanation.
|
||||
// classmethod-like or staticmethod-like). See "Decorating a method with
|
||||
// a `Callable`-typed decorator" in `callables_as_descriptors.md` for the
|
||||
// extended explanation.
|
||||
return_ty_modified
|
||||
} else {
|
||||
return_ty
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue