From 1a067431e80d995a4a5239748309892a326dad53 Mon Sep 17 00:00:00 2001 From: med Date: Thu, 19 Jun 2025 17:06:37 -0700 Subject: [PATCH] add staticmethod support --- .../resources/mdtest/call/methods.md | 108 ++++++++++++++++++ .../resources/mdtest/descriptor_protocol.md | 13 +++ .../resources/mdtest/overloads.md | 8 +- .../ty_python_semantic/src/types/call/bind.rs | 7 ++ crates/ty_python_semantic/src/types/class.rs | 9 ++ .../ty_python_semantic/src/types/function.rs | 2 + crates/ty_python_semantic/src/types/infer.rs | 17 ++- 7 files changed, 156 insertions(+), 8 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/methods.md b/crates/ty_python_semantic/resources/mdtest/call/methods.md index e6fde2e2617e0..8b178407c75bf 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/methods.md +++ b/crates/ty_python_semantic/resources/mdtest/call/methods.md @@ -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 diff --git a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md index 86b16e4202673..cabea0ce7da81 100644 --- a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md @@ -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 diff --git a/crates/ty_python_semantic/resources/mdtest/overloads.md b/crates/ty_python_semantic/resources/mdtest/overloads.md index c26b9cf42b8ec..650dd9da44845 100644 --- a/crates/ty_python_semantic/resources/mdtest/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/overloads.md @@ -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 diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index fb41f559af469..c72180daf11fa 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -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)); @@ -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) => { diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 4c729caee92f9..243e66b3a8627 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -2119,6 +2119,7 @@ pub enum KnownClass { Exception, BaseExceptionGroup, ExceptionGroup, + Staticmethod, Classmethod, Super, // enum @@ -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 @@ -2293,6 +2295,7 @@ impl<'db> KnownClass { | Self::BaseExceptionGroup | Self::Exception | Self::ExceptionGroup + | Self::Staticmethod | Self::Classmethod | Self::GenericAlias | Self::GeneratorType @@ -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", @@ -2578,6 +2582,7 @@ impl<'db> KnownClass { | Self::BaseExceptionGroup | Self::Exception | Self::ExceptionGroup + | Self::Staticmethod | Self::Classmethod | Self::Slice | Self::Super @@ -2672,6 +2677,7 @@ impl<'db> KnownClass { | Self::BaseExceptionGroup | Self::Exception | Self::ExceptionGroup + | Self::Staticmethod | Self::Classmethod | Self::GenericAlias | Self::ModuleType @@ -2754,6 +2760,7 @@ impl<'db> KnownClass { | Self::BaseExceptionGroup | Self::Exception | Self::ExceptionGroup + | Self::Staticmethod | Self::Classmethod | Self::TypeVar | Self::ParamSpec @@ -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, @@ -2885,6 +2893,7 @@ impl<'db> KnownClass { | Self::ExceptionGroup | Self::EllipsisType | Self::BaseExceptionGroup + | Self::Staticmethod | Self::Classmethod | Self::FunctionType | Self::MethodType diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index ae693bf731433..f09ff5dbcd452 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -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; } diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 4f7180ee31bde..73d34d7a884c2 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -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![]; @@ -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); }