Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/call/methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions crates/ty_python_semantic/resources/mdtest/overloads.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions crates/ty_python_semantic/src/types/call/bind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,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() {
if first.is_none(db) {
overload.set_return_type(Type::FunctionLiteral(function));
Expand Down Expand Up @@ -316,6 +319,10 @@ impl<'db> Bindings<'db> {

_ => {}
}
} else if function
.has_known_decorator(db, FunctionDecorators::STATICMETHOD)
{
overload.set_return_type(*function_ty);
} else {
match overload.parameter_types() {
[_, Some(instance), _] if instance.is_none(db) => {
Expand Down
9 changes: 9 additions & 0 deletions crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2119,6 +2119,7 @@ pub enum KnownClass {
Exception,
BaseExceptionGroup,
ExceptionGroup,
Staticmethod,
Classmethod,
Super,
// enum
Expand Down Expand Up @@ -2249,6 +2250,7 @@ impl<'db> KnownClass {
// and raises a `TypeError` in Python >=3.14
// (see https://docs.python.org/3/library/constants.html#NotImplemented)
| Self::NotImplementedType
| Self::Staticmethod
| Self::Classmethod
| Self::Field
| Self::KwOnly
Expand Down Expand Up @@ -2293,6 +2295,7 @@ impl<'db> KnownClass {
| Self::BaseExceptionGroup
| Self::Exception
| Self::ExceptionGroup
| Self::Staticmethod
| Self::Classmethod
| Self::GenericAlias
| Self::GeneratorType
Expand Down Expand Up @@ -2355,6 +2358,7 @@ impl<'db> KnownClass {
Self::BaseExceptionGroup => "BaseExceptionGroup",
Self::Exception => "Exception",
Self::ExceptionGroup => "ExceptionGroup",
Self::Staticmethod => "staticmethod",
Self::Classmethod => "classmethod",
Self::GenericAlias => "GenericAlias",
Self::ModuleType => "ModuleType",
Expand Down Expand Up @@ -2578,6 +2582,7 @@ impl<'db> KnownClass {
| Self::BaseExceptionGroup
| Self::Exception
| Self::ExceptionGroup
| Self::Staticmethod
| Self::Classmethod
| Self::Slice
| Self::Super
Expand Down Expand Up @@ -2672,6 +2677,7 @@ impl<'db> KnownClass {
| Self::BaseExceptionGroup
| Self::Exception
| Self::ExceptionGroup
| Self::Staticmethod
| Self::Classmethod
| Self::GenericAlias
| Self::ModuleType
Expand Down Expand Up @@ -2754,6 +2760,7 @@ impl<'db> KnownClass {
| Self::BaseExceptionGroup
| Self::Exception
| Self::ExceptionGroup
| Self::Staticmethod
| Self::Classmethod
| Self::TypeVar
| Self::ParamSpec
Expand Down Expand Up @@ -2801,6 +2808,7 @@ impl<'db> KnownClass {
"BaseExceptionGroup" => Self::BaseExceptionGroup,
"Exception" => Self::Exception,
"ExceptionGroup" => Self::ExceptionGroup,
"staticmethod" => Self::Staticmethod,
"classmethod" => Self::Classmethod,
"GenericAlias" => Self::GenericAlias,
"NoneType" => Self::NoneType,
Expand Down Expand Up @@ -2885,6 +2893,7 @@ impl<'db> KnownClass {
| Self::ExceptionGroup
| Self::EllipsisType
| Self::BaseExceptionGroup
| Self::Staticmethod
| Self::Classmethod
| Self::FunctionType
| Self::MethodType
Expand Down
2 changes: 2 additions & 0 deletions crates/ty_python_semantic/src/types/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ bitflags! {
const ABSTRACT_METHOD = 1 << 3;
/// `@typing.final`
const FINAL = 1 << 4;
/// `@staticmethod`
const STATICMETHOD = 1 << 5;
/// `@typing.override`
const OVERRIDE = 1 << 6;
}
Expand Down
17 changes: 12 additions & 5 deletions crates/ty_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1237,8 +1237,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}

// TODO: Add `@staticmethod`
for (decorator, name) in [(FunctionDecorators::CLASSMETHOD, "classmethod")] {
for (decorator, name) in [
(FunctionDecorators::CLASSMETHOD, "classmethod"),
(FunctionDecorators::STATICMETHOD, "staticmethod"),
] {
let mut decorator_present = false;
let mut decorator_missing = vec![];

Expand Down Expand Up @@ -2155,12 +2157,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
_ => {}
}
}
Type::ClassLiteral(class) => {
if class.is_known(self.db(), KnownClass::Classmethod) {
Type::ClassLiteral(class) => match class.known(self.db()) {
Some(KnownClass::Classmethod) => {
function_decorators |= FunctionDecorators::CLASSMETHOD;
continue;
}
}
Some(KnownClass::Staticmethod) => {
function_decorators |= FunctionDecorators::STATICMETHOD;
continue;
}
_ => {}
},
Type::DataclassTransformer(params) => {
dataclass_transformer_params = Some(params);
}
Expand Down
Loading