Skip to content

Commit 7982eda

Browse files
authored
[ty] Add support for @staticmethods (#18809)
## 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 astral-sh/ty#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.
1 parent e180975 commit 7982eda

File tree

7 files changed

+156
-8
lines changed

7 files changed

+156
-8
lines changed

crates/ty_python_semantic/resources/mdtest/call/methods.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,4 +430,112 @@ reveal_type(C.f2(1)) # revealed: str
430430
reveal_type(C().f2(1)) # revealed: str
431431
```
432432

433+
## `@staticmethod`
434+
435+
### Basic
436+
437+
When a `@staticmethod` attribute is accessed, it returns the underlying function object. This is
438+
true whether it's accessed on the class or on an instance of the class.
439+
440+
```py
441+
from __future__ import annotations
442+
443+
class C:
444+
@staticmethod
445+
def f(x: int) -> str:
446+
return "a"
447+
448+
reveal_type(C.f) # revealed: def f(x: int) -> str
449+
reveal_type(C().f) # revealed: def f(x: int) -> str
450+
```
451+
452+
The method can then be called like a regular function from either the class or an instance, with no
453+
implicit first argument passed.
454+
455+
```py
456+
reveal_type(C.f(1)) # revealed: str
457+
reveal_type(C().f(1)) # revealed: str
458+
```
459+
460+
When the static method is called incorrectly, we detect it:
461+
462+
```py
463+
C.f("incorrect") # error: [invalid-argument-type]
464+
C.f() # error: [missing-argument]
465+
C.f(1, 2) # error: [too-many-positional-arguments]
466+
```
467+
468+
When a static method is accessed on a derived class, it behaves identically:
469+
470+
```py
471+
class Derived(C):
472+
pass
473+
474+
reveal_type(Derived.f) # revealed: def f(x: int) -> str
475+
reveal_type(Derived().f) # revealed: def f(x: int) -> str
476+
477+
reveal_type(Derived.f(1)) # revealed: str
478+
reveal_type(Derived().f(1)) # revealed: str
479+
```
480+
481+
### Accessing the staticmethod as a static member
482+
483+
```py
484+
from inspect import getattr_static
485+
486+
class C:
487+
@staticmethod
488+
def f(): ...
489+
```
490+
491+
Accessing the staticmethod as a static member. This will reveal the raw function, as `staticmethod`
492+
is transparent when accessed via `getattr_static`.
493+
494+
```py
495+
reveal_type(getattr_static(C, "f")) # revealed: def f() -> Unknown
496+
```
497+
498+
The `__get__` of a `staticmethod` object simply returns the underlying function. It ignores both the
499+
instance and owner arguments.
500+
501+
```py
502+
reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: def f() -> Unknown
503+
reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: def f() -> Unknown
504+
reveal_type(getattr_static(C, "f").__get__(C())) # revealed: def f() -> Unknown
505+
reveal_type(getattr_static(C, "f").__get__("dummy", C)) # revealed: def f() -> Unknown
506+
```
507+
508+
### Staticmethods mixed with other decorators
509+
510+
```toml
511+
[environment]
512+
python-version = "3.12"
513+
```
514+
515+
When a `@staticmethod` is additionally decorated with another decorator, it is still treated as a
516+
static method:
517+
518+
```py
519+
from __future__ import annotations
520+
521+
def does_nothing[T](f: T) -> T:
522+
return f
523+
524+
class C:
525+
@staticmethod
526+
@does_nothing
527+
def f1(x: int) -> str:
528+
return "a"
529+
530+
@does_nothing
531+
@staticmethod
532+
def f2(x: int) -> str:
533+
return "a"
534+
535+
reveal_type(C.f1(1)) # revealed: str
536+
reveal_type(C().f1(1)) # revealed: str
537+
reveal_type(C.f2(1)) # revealed: str
538+
reveal_type(C().f2(1)) # revealed: str
539+
```
540+
433541
[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods

crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,19 @@ reveal_type(C.get_name()) # revealed: str
549549
reveal_type(C("42").get_name()) # revealed: str
550550
```
551551

552+
### Built-in `staticmethod` descriptor
553+
554+
```py
555+
class C:
556+
@staticmethod
557+
def helper(value: str) -> str:
558+
return value
559+
560+
reveal_type(C.helper("42")) # revealed: str
561+
c = C()
562+
reveal_type(c.helper("string")) # revealed: str
563+
```
564+
552565
### Functions as descriptors
553566

554567
Functions are descriptors because they implement a `__get__` method. This is crucial in making sure

crates/ty_python_semantic/resources/mdtest/overloads.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -449,30 +449,32 @@ from __future__ import annotations
449449
from typing import overload
450450

451451
class CheckStaticMethod:
452-
# TODO: error because `@staticmethod` does not exist on all overloads
453452
@overload
454453
def method1(x: int) -> int: ...
455454
@overload
456455
def method1(x: str) -> str: ...
457456
@staticmethod
457+
# error: [invalid-overload] "Overloaded function `method1` does not use the `@staticmethod` decorator consistently"
458458
def method1(x: int | str) -> int | str:
459459
return x
460-
# TODO: error because `@staticmethod` does not exist on all overloads
460+
461461
@overload
462462
def method2(x: int) -> int: ...
463463
@overload
464464
@staticmethod
465465
def method2(x: str) -> str: ...
466466
@staticmethod
467+
# error: [invalid-overload]
467468
def method2(x: int | str) -> int | str:
468469
return x
469-
# TODO: error because `@staticmethod` does not exist on the implementation
470+
470471
@overload
471472
@staticmethod
472473
def method3(x: int) -> int: ...
473474
@overload
474475
@staticmethod
475476
def method3(x: str) -> str: ...
477+
# error: [invalid-overload]
476478
def method3(x: int | str) -> int | str:
477479
return x
478480

crates/ty_python_semantic/src/types/call/bind.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,9 @@ impl<'db> Bindings<'db> {
284284
}
285285
_ => {}
286286
}
287+
} else if function.has_known_decorator(db, FunctionDecorators::STATICMETHOD)
288+
{
289+
overload.set_return_type(Type::FunctionLiteral(function));
287290
} else if let [Some(first), _] = overload.parameter_types() {
288291
if first.is_none(db) {
289292
overload.set_return_type(Type::FunctionLiteral(function));
@@ -319,6 +322,10 @@ impl<'db> Bindings<'db> {
319322

320323
_ => {}
321324
}
325+
} else if function
326+
.has_known_decorator(db, FunctionDecorators::STATICMETHOD)
327+
{
328+
overload.set_return_type(*function_ty);
322329
} else {
323330
match overload.parameter_types() {
324331
[_, Some(instance), _] if instance.is_none(db) => {

crates/ty_python_semantic/src/types/class.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2119,6 +2119,7 @@ pub enum KnownClass {
21192119
Exception,
21202120
BaseExceptionGroup,
21212121
ExceptionGroup,
2122+
Staticmethod,
21222123
Classmethod,
21232124
Super,
21242125
// enum
@@ -2249,6 +2250,7 @@ impl<'db> KnownClass {
22492250
// and raises a `TypeError` in Python >=3.14
22502251
// (see https://docs.python.org/3/library/constants.html#NotImplemented)
22512252
| Self::NotImplementedType
2253+
| Self::Staticmethod
22522254
| Self::Classmethod
22532255
| Self::Field
22542256
| Self::KwOnly
@@ -2293,6 +2295,7 @@ impl<'db> KnownClass {
22932295
| Self::BaseExceptionGroup
22942296
| Self::Exception
22952297
| Self::ExceptionGroup
2298+
| Self::Staticmethod
22962299
| Self::Classmethod
22972300
| Self::GenericAlias
22982301
| Self::GeneratorType
@@ -2355,6 +2358,7 @@ impl<'db> KnownClass {
23552358
Self::BaseExceptionGroup => "BaseExceptionGroup",
23562359
Self::Exception => "Exception",
23572360
Self::ExceptionGroup => "ExceptionGroup",
2361+
Self::Staticmethod => "staticmethod",
23582362
Self::Classmethod => "classmethod",
23592363
Self::GenericAlias => "GenericAlias",
23602364
Self::ModuleType => "ModuleType",
@@ -2578,6 +2582,7 @@ impl<'db> KnownClass {
25782582
| Self::BaseExceptionGroup
25792583
| Self::Exception
25802584
| Self::ExceptionGroup
2585+
| Self::Staticmethod
25812586
| Self::Classmethod
25822587
| Self::Slice
25832588
| Self::Super
@@ -2672,6 +2677,7 @@ impl<'db> KnownClass {
26722677
| Self::BaseExceptionGroup
26732678
| Self::Exception
26742679
| Self::ExceptionGroup
2680+
| Self::Staticmethod
26752681
| Self::Classmethod
26762682
| Self::GenericAlias
26772683
| Self::ModuleType
@@ -2754,6 +2760,7 @@ impl<'db> KnownClass {
27542760
| Self::BaseExceptionGroup
27552761
| Self::Exception
27562762
| Self::ExceptionGroup
2763+
| Self::Staticmethod
27572764
| Self::Classmethod
27582765
| Self::TypeVar
27592766
| Self::ParamSpec
@@ -2801,6 +2808,7 @@ impl<'db> KnownClass {
28012808
"BaseExceptionGroup" => Self::BaseExceptionGroup,
28022809
"Exception" => Self::Exception,
28032810
"ExceptionGroup" => Self::ExceptionGroup,
2811+
"staticmethod" => Self::Staticmethod,
28042812
"classmethod" => Self::Classmethod,
28052813
"GenericAlias" => Self::GenericAlias,
28062814
"NoneType" => Self::NoneType,
@@ -2885,6 +2893,7 @@ impl<'db> KnownClass {
28852893
| Self::ExceptionGroup
28862894
| Self::EllipsisType
28872895
| Self::BaseExceptionGroup
2896+
| Self::Staticmethod
28882897
| Self::Classmethod
28892898
| Self::FunctionType
28902899
| Self::MethodType

crates/ty_python_semantic/src/types/function.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ bitflags! {
103103
const ABSTRACT_METHOD = 1 << 3;
104104
/// `@typing.final`
105105
const FINAL = 1 << 4;
106+
/// `@staticmethod`
107+
const STATICMETHOD = 1 << 5;
106108
/// `@typing.override`
107109
const OVERRIDE = 1 << 6;
108110
}

crates/ty_python_semantic/src/types/infer.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1277,8 +1277,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
12771277
}
12781278
}
12791279

1280-
// TODO: Add `@staticmethod`
1281-
for (decorator, name) in [(FunctionDecorators::CLASSMETHOD, "classmethod")] {
1280+
for (decorator, name) in [
1281+
(FunctionDecorators::CLASSMETHOD, "classmethod"),
1282+
(FunctionDecorators::STATICMETHOD, "staticmethod"),
1283+
] {
12821284
let mut decorator_present = false;
12831285
let mut decorator_missing = vec![];
12841286

@@ -2195,12 +2197,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
21952197
_ => {}
21962198
}
21972199
}
2198-
Type::ClassLiteral(class) => {
2199-
if class.is_known(self.db(), KnownClass::Classmethod) {
2200+
Type::ClassLiteral(class) => match class.known(self.db()) {
2201+
Some(KnownClass::Classmethod) => {
22002202
function_decorators |= FunctionDecorators::CLASSMETHOD;
22012203
continue;
22022204
}
2203-
}
2205+
Some(KnownClass::Staticmethod) => {
2206+
function_decorators |= FunctionDecorators::STATICMETHOD;
2207+
continue;
2208+
}
2209+
_ => {}
2210+
},
22042211
Type::DataclassTransformer(params) => {
22052212
dataclass_transformer_params = Some(params);
22062213
}

0 commit comments

Comments
 (0)