This commit is contained in:
Charlie Marsh 2025-12-23 09:56:56 +01:00 committed by GitHub
commit e67fbf0f78
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 104 additions and 12 deletions

View file

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

View file

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

View file

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

View file

@ -185,7 +185,7 @@ class LiskovViolatingButNotOverrideViolating(Parent):
@staticmethod
@override
def class_method1() -> int: ...
def class_method1() -> int: ... # error: [invalid-method-override]
@classmethod
@override

View file

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

View file

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

View file

@ -4687,6 +4687,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) =>
{
@ -12387,6 +12392,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,
@ -12491,6 +12500,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,

View file

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

View file

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