[ty] Add support for @staticmethods (#18809)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / mkdocs (push) Waiting to run
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

## Summary

Add support for `@staticmethod`s. Overall, the changes are very similar
to #16305.

#18587 will be dependent on this PR for a potential fix of
https://github.com/astral-sh/ty/issues/207.

mypy_primer will look bad since the new code allows ty to check more
code.

## Test Plan

Added new markdown tests. Please comment if there's any missing tests
that I should add in, thank you.
This commit is contained in:
med1844 2025-06-20 01:38:17 -07:00 committed by GitHub
parent e180975226
commit 7982edac90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 156 additions and 8 deletions

View file

@ -430,4 +430,112 @@ reveal_type(C.f2(1)) # revealed: str
reveal_type(C().f2(1)) # revealed: str
```
## `@staticmethod`
### Basic
When a `@staticmethod` attribute is accessed, it returns the underlying function object. This is
true whether it's accessed on the class or on an instance of the class.
```py
from __future__ import annotations
class C:
@staticmethod
def f(x: int) -> str:
return "a"
reveal_type(C.f) # revealed: def f(x: int) -> str
reveal_type(C().f) # revealed: def f(x: int) -> str
```
The method can then be called like a regular function from either the class or an instance, with no
implicit first argument passed.
```py
reveal_type(C.f(1)) # revealed: str
reveal_type(C().f(1)) # revealed: str
```
When the static 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]
```
When a static method is accessed on a derived class, it behaves identically:
```py
class Derived(C):
pass
reveal_type(Derived.f) # revealed: def f(x: int) -> str
reveal_type(Derived().f) # revealed: def f(x: int) -> str
reveal_type(Derived.f(1)) # revealed: str
reveal_type(Derived().f(1)) # revealed: str
```
### Accessing the staticmethod as a static member
```py
from inspect import getattr_static
class C:
@staticmethod
def f(): ...
```
Accessing the staticmethod as a static member. This will reveal the raw function, as `staticmethod`
is transparent when accessed via `getattr_static`.
```py
reveal_type(getattr_static(C, "f")) # revealed: def f() -> Unknown
```
The `__get__` of a `staticmethod` object simply returns the underlying function. It ignores both the
instance and owner arguments.
```py
reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: def f() -> Unknown
reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: def f() -> Unknown
reveal_type(getattr_static(C, "f").__get__(C())) # revealed: def f() -> Unknown
reveal_type(getattr_static(C, "f").__get__("dummy", C)) # revealed: def f() -> Unknown
```
### Staticmethods mixed with other decorators
```toml
[environment]
python-version = "3.12"
```
When a `@staticmethod` is additionally decorated with another decorator, it is still treated as a
static method:
```py
from __future__ import annotations
def does_nothing[T](f: T) -> T:
return f
class C:
@staticmethod
@does_nothing
def f1(x: int) -> str:
return "a"
@does_nothing
@staticmethod
def f2(x: int) -> str:
return "a"
reveal_type(C.f1(1)) # revealed: str
reveal_type(C().f1(1)) # revealed: str
reveal_type(C.f2(1)) # revealed: str
reveal_type(C().f2(1)) # revealed: str
```
[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods

View file

@ -549,6 +549,19 @@ reveal_type(C.get_name()) # revealed: str
reveal_type(C("42").get_name()) # revealed: str
```
### Built-in `staticmethod` descriptor
```py
class C:
@staticmethod
def helper(value: str) -> str:
return value
reveal_type(C.helper("42")) # revealed: str
c = C()
reveal_type(c.helper("string")) # revealed: str
```
### Functions as descriptors
Functions are descriptors because they implement a `__get__` method. This is crucial in making sure

View file

@ -449,30 +449,32 @@ from __future__ import annotations
from typing import overload
class CheckStaticMethod:
# TODO: error because `@staticmethod` does not exist on all overloads
@overload
def method1(x: int) -> int: ...
@overload
def method1(x: str) -> str: ...
@staticmethod
# error: [invalid-overload] "Overloaded function `method1` does not use the `@staticmethod` decorator consistently"
def method1(x: int | str) -> int | str:
return x
# TODO: error because `@staticmethod` does not exist on all overloads
@overload
def method2(x: int) -> int: ...
@overload
@staticmethod
def method2(x: str) -> str: ...
@staticmethod
# error: [invalid-overload]
def method2(x: int | str) -> int | str:
return x
# TODO: error because `@staticmethod` does not exist on the implementation
@overload
@staticmethod
def method3(x: int) -> int: ...
@overload
@staticmethod
def method3(x: str) -> str: ...
# error: [invalid-overload]
def method3(x: int | str) -> int | str:
return x