mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-21 19:05:09 +00:00
[ty] Add support for @staticmethod
s (#18809)
Some checks are pending
CI / cargo build (msrv) (push) Blocked by required conditions
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 fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
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 / mkdocs (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
Some checks are pending
CI / cargo build (msrv) (push) Blocked by required conditions
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 fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
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 / mkdocs (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:
parent
e180975226
commit
7982edac90
7 changed files with 156 additions and 8 deletions
|
@ -430,4 +430,112 @@ reveal_type(C.f2(1)) # revealed: str
|
||||||
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
|
[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods
|
||||||
|
|
|
@ -549,6 +549,19 @@ reveal_type(C.get_name()) # revealed: str
|
||||||
reveal_type(C("42").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 as descriptors
|
||||||
|
|
||||||
Functions are descriptors because they implement a `__get__` method. This is crucial in making sure
|
Functions are descriptors because they implement a `__get__` method. This is crucial in making sure
|
||||||
|
|
|
@ -449,30 +449,32 @@ from __future__ import annotations
|
||||||
from typing import overload
|
from typing import overload
|
||||||
|
|
||||||
class CheckStaticMethod:
|
class CheckStaticMethod:
|
||||||
# TODO: error because `@staticmethod` does not exist on all overloads
|
|
||||||
@overload
|
@overload
|
||||||
def method1(x: int) -> int: ...
|
def method1(x: int) -> int: ...
|
||||||
@overload
|
@overload
|
||||||
def method1(x: str) -> str: ...
|
def method1(x: str) -> str: ...
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
# error: [invalid-overload] "Overloaded function `method1` does not use the `@staticmethod` decorator consistently"
|
||||||
def method1(x: int | str) -> int | str:
|
def method1(x: int | str) -> int | str:
|
||||||
return x
|
return x
|
||||||
# TODO: error because `@staticmethod` does not exist on all overloads
|
|
||||||
@overload
|
@overload
|
||||||
def method2(x: int) -> int: ...
|
def method2(x: int) -> int: ...
|
||||||
@overload
|
@overload
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def method2(x: str) -> str: ...
|
def method2(x: str) -> str: ...
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
# error: [invalid-overload]
|
||||||
def method2(x: int | str) -> int | str:
|
def method2(x: int | str) -> int | str:
|
||||||
return x
|
return x
|
||||||
# TODO: error because `@staticmethod` does not exist on the implementation
|
|
||||||
@overload
|
@overload
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def method3(x: int) -> int: ...
|
def method3(x: int) -> int: ...
|
||||||
@overload
|
@overload
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def method3(x: str) -> str: ...
|
def method3(x: str) -> str: ...
|
||||||
|
# error: [invalid-overload]
|
||||||
def method3(x: int | str) -> int | str:
|
def method3(x: int | str) -> int | str:
|
||||||
return x
|
return x
|
||||||
|
|
||||||
|
|
|
@ -284,6 +284,9 @@ impl<'db> Bindings<'db> {
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
} else if function.has_known_decorator(db, FunctionDecorators::STATICMETHOD)
|
||||||
|
{
|
||||||
|
overload.set_return_type(Type::FunctionLiteral(function));
|
||||||
} else if let [Some(first), _] = overload.parameter_types() {
|
} else if let [Some(first), _] = overload.parameter_types() {
|
||||||
if first.is_none(db) {
|
if first.is_none(db) {
|
||||||
overload.set_return_type(Type::FunctionLiteral(function));
|
overload.set_return_type(Type::FunctionLiteral(function));
|
||||||
|
@ -319,6 +322,10 @@ impl<'db> Bindings<'db> {
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
} else if function
|
||||||
|
.has_known_decorator(db, FunctionDecorators::STATICMETHOD)
|
||||||
|
{
|
||||||
|
overload.set_return_type(*function_ty);
|
||||||
} else {
|
} else {
|
||||||
match overload.parameter_types() {
|
match overload.parameter_types() {
|
||||||
[_, Some(instance), _] if instance.is_none(db) => {
|
[_, Some(instance), _] if instance.is_none(db) => {
|
||||||
|
|
|
@ -2119,6 +2119,7 @@ pub enum KnownClass {
|
||||||
Exception,
|
Exception,
|
||||||
BaseExceptionGroup,
|
BaseExceptionGroup,
|
||||||
ExceptionGroup,
|
ExceptionGroup,
|
||||||
|
Staticmethod,
|
||||||
Classmethod,
|
Classmethod,
|
||||||
Super,
|
Super,
|
||||||
// enum
|
// enum
|
||||||
|
@ -2249,6 +2250,7 @@ impl<'db> KnownClass {
|
||||||
// and raises a `TypeError` in Python >=3.14
|
// and raises a `TypeError` in Python >=3.14
|
||||||
// (see https://docs.python.org/3/library/constants.html#NotImplemented)
|
// (see https://docs.python.org/3/library/constants.html#NotImplemented)
|
||||||
| Self::NotImplementedType
|
| Self::NotImplementedType
|
||||||
|
| Self::Staticmethod
|
||||||
| Self::Classmethod
|
| Self::Classmethod
|
||||||
| Self::Field
|
| Self::Field
|
||||||
| Self::KwOnly
|
| Self::KwOnly
|
||||||
|
@ -2293,6 +2295,7 @@ impl<'db> KnownClass {
|
||||||
| Self::BaseExceptionGroup
|
| Self::BaseExceptionGroup
|
||||||
| Self::Exception
|
| Self::Exception
|
||||||
| Self::ExceptionGroup
|
| Self::ExceptionGroup
|
||||||
|
| Self::Staticmethod
|
||||||
| Self::Classmethod
|
| Self::Classmethod
|
||||||
| Self::GenericAlias
|
| Self::GenericAlias
|
||||||
| Self::GeneratorType
|
| Self::GeneratorType
|
||||||
|
@ -2355,6 +2358,7 @@ impl<'db> KnownClass {
|
||||||
Self::BaseExceptionGroup => "BaseExceptionGroup",
|
Self::BaseExceptionGroup => "BaseExceptionGroup",
|
||||||
Self::Exception => "Exception",
|
Self::Exception => "Exception",
|
||||||
Self::ExceptionGroup => "ExceptionGroup",
|
Self::ExceptionGroup => "ExceptionGroup",
|
||||||
|
Self::Staticmethod => "staticmethod",
|
||||||
Self::Classmethod => "classmethod",
|
Self::Classmethod => "classmethod",
|
||||||
Self::GenericAlias => "GenericAlias",
|
Self::GenericAlias => "GenericAlias",
|
||||||
Self::ModuleType => "ModuleType",
|
Self::ModuleType => "ModuleType",
|
||||||
|
@ -2578,6 +2582,7 @@ impl<'db> KnownClass {
|
||||||
| Self::BaseExceptionGroup
|
| Self::BaseExceptionGroup
|
||||||
| Self::Exception
|
| Self::Exception
|
||||||
| Self::ExceptionGroup
|
| Self::ExceptionGroup
|
||||||
|
| Self::Staticmethod
|
||||||
| Self::Classmethod
|
| Self::Classmethod
|
||||||
| Self::Slice
|
| Self::Slice
|
||||||
| Self::Super
|
| Self::Super
|
||||||
|
@ -2672,6 +2677,7 @@ impl<'db> KnownClass {
|
||||||
| Self::BaseExceptionGroup
|
| Self::BaseExceptionGroup
|
||||||
| Self::Exception
|
| Self::Exception
|
||||||
| Self::ExceptionGroup
|
| Self::ExceptionGroup
|
||||||
|
| Self::Staticmethod
|
||||||
| Self::Classmethod
|
| Self::Classmethod
|
||||||
| Self::GenericAlias
|
| Self::GenericAlias
|
||||||
| Self::ModuleType
|
| Self::ModuleType
|
||||||
|
@ -2754,6 +2760,7 @@ impl<'db> KnownClass {
|
||||||
| Self::BaseExceptionGroup
|
| Self::BaseExceptionGroup
|
||||||
| Self::Exception
|
| Self::Exception
|
||||||
| Self::ExceptionGroup
|
| Self::ExceptionGroup
|
||||||
|
| Self::Staticmethod
|
||||||
| Self::Classmethod
|
| Self::Classmethod
|
||||||
| Self::TypeVar
|
| Self::TypeVar
|
||||||
| Self::ParamSpec
|
| Self::ParamSpec
|
||||||
|
@ -2801,6 +2808,7 @@ impl<'db> KnownClass {
|
||||||
"BaseExceptionGroup" => Self::BaseExceptionGroup,
|
"BaseExceptionGroup" => Self::BaseExceptionGroup,
|
||||||
"Exception" => Self::Exception,
|
"Exception" => Self::Exception,
|
||||||
"ExceptionGroup" => Self::ExceptionGroup,
|
"ExceptionGroup" => Self::ExceptionGroup,
|
||||||
|
"staticmethod" => Self::Staticmethod,
|
||||||
"classmethod" => Self::Classmethod,
|
"classmethod" => Self::Classmethod,
|
||||||
"GenericAlias" => Self::GenericAlias,
|
"GenericAlias" => Self::GenericAlias,
|
||||||
"NoneType" => Self::NoneType,
|
"NoneType" => Self::NoneType,
|
||||||
|
@ -2885,6 +2893,7 @@ impl<'db> KnownClass {
|
||||||
| Self::ExceptionGroup
|
| Self::ExceptionGroup
|
||||||
| Self::EllipsisType
|
| Self::EllipsisType
|
||||||
| Self::BaseExceptionGroup
|
| Self::BaseExceptionGroup
|
||||||
|
| Self::Staticmethod
|
||||||
| Self::Classmethod
|
| Self::Classmethod
|
||||||
| Self::FunctionType
|
| Self::FunctionType
|
||||||
| Self::MethodType
|
| Self::MethodType
|
||||||
|
|
|
@ -103,6 +103,8 @@ bitflags! {
|
||||||
const ABSTRACT_METHOD = 1 << 3;
|
const ABSTRACT_METHOD = 1 << 3;
|
||||||
/// `@typing.final`
|
/// `@typing.final`
|
||||||
const FINAL = 1 << 4;
|
const FINAL = 1 << 4;
|
||||||
|
/// `@staticmethod`
|
||||||
|
const STATICMETHOD = 1 << 5;
|
||||||
/// `@typing.override`
|
/// `@typing.override`
|
||||||
const OVERRIDE = 1 << 6;
|
const OVERRIDE = 1 << 6;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1277,8 +1277,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add `@staticmethod`
|
for (decorator, name) in [
|
||||||
for (decorator, name) in [(FunctionDecorators::CLASSMETHOD, "classmethod")] {
|
(FunctionDecorators::CLASSMETHOD, "classmethod"),
|
||||||
|
(FunctionDecorators::STATICMETHOD, "staticmethod"),
|
||||||
|
] {
|
||||||
let mut decorator_present = false;
|
let mut decorator_present = false;
|
||||||
let mut decorator_missing = vec![];
|
let mut decorator_missing = vec![];
|
||||||
|
|
||||||
|
@ -2195,12 +2197,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Type::ClassLiteral(class) => {
|
Type::ClassLiteral(class) => match class.known(self.db()) {
|
||||||
if class.is_known(self.db(), KnownClass::Classmethod) {
|
Some(KnownClass::Classmethod) => {
|
||||||
function_decorators |= FunctionDecorators::CLASSMETHOD;
|
function_decorators |= FunctionDecorators::CLASSMETHOD;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
Some(KnownClass::Staticmethod) => {
|
||||||
|
function_decorators |= FunctionDecorators::STATICMETHOD;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
Type::DataclassTransformer(params) => {
|
Type::DataclassTransformer(params) => {
|
||||||
dataclass_transformer_params = Some(params);
|
dataclass_transformer_params = Some(params);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue