Repository: python/mypy

Issue:
stubgenc.py does not generate correct lines for staticmethods
**Bug Report**

When I use stubgen to generate stub file for a pybind11 module

```cpp
klass
.def_static("some_static_method", &A::some_static_method,
    R"#(None)#", py::arg("a"), py::arg("b")
    );
```
where `pybind11::class_ klass` is bound to C++ `class A`

the result line is like:

```python
class A:
    def some_static_method(self, *args, **kwargs): ...
```

which should be like:

```python
class A:
    @static_method
    def some_static_method(a: T, b: T): ...
```

The issue here is related to a pybind11 discussion: [https://github.com/pybind/pybind11/issues/2403].
which says "a static method of class Foo, Foo.__dict__['f_static'] points to the static method decorator around the function object instead, not the function object itself"

So, I temporarily fixed this issue for my own project by appending a staticmethod checker right after the `is_c_classmethod` in file stubgenc.py:

```python
def is_c_staticmethod(obj: object) -> bool:
    return type(obj).__name__ in ('staticmethod')
```

```python
def generate_c_type_stub(module: ModuleType,
                         class_name: str,
                         obj: type,
                         output: List[str],
                         imports: List[str],
                         sigs: Optional[Dict[str, str]] = None,
                         class_sigs: Optional[Dict[str, str]] = None) -> None:
                ...
                if is_c_classmethod(value):
                    methods.append('@classmethod')
                    self_var = 'cls'
                elif is_c_staticmethod(value):
                    methods.append('@staticmethod')
                    value = value.__func__
                    self_var = None
                else:
                    self_var = 'self'
                ...
```
The result turns out correct with my pybind11 code. But I guess it may not work for other conditions?








Discussion:
For overloaded static methods the `@staticmethod` decorator is added to the output only once at the top, because multiple overloads are inferred in `value.__func__` and their stubs appended in a loop in function `generate_c_function_stub`:

```python
class A:
    @staticmethod
    @overload
    def some_static_method(a: A1, b: B1): ...
    @overload
    def some_static_method(a: A2, b: B2): ...
    @overload
    def some_static_method(a: A3, b: B3): ...
```

Passing an additional decorator string argument to `generate_c_function_stub`, and use it in that loop can fix this problem:

```python
class A:
    @overload
    @staticmethod
    def some_static_method(a: A1, b: B1): ...
    @overload
    @staticmethod
    def some_static_method(a: A2, b: B2): ...
    @overload
    @staticmethod
    def some_static_method(a: A3, b: B3): ...
```


Resolving patch:
diff --git a/mypy/stubgenc.py b/mypy/stubgenc.py
--- a/mypy/stubgenc.py
+++ b/mypy/stubgenc.py
@@ -530,12 +530,14 @@ def is_classmethod(self, class_info: ClassInfo, name: str, obj: object) -> bool:
             return inspect.ismethod(obj)
 
     def is_staticmethod(self, class_info: ClassInfo | None, name: str, obj: object) -> bool:
-        if self.is_c_module:
+        if class_info is None:
             return False
+        elif self.is_c_module:
+            raw_lookup: Mapping[str, Any] = getattr(class_info.cls, "__dict__")  # noqa: B009
+            raw_value = raw_lookup.get(name, obj)
+            return isinstance(raw_value, staticmethod)
         else:
-            return class_info is not None and isinstance(
-                inspect.getattr_static(class_info.cls, name), staticmethod
-            )
+            return isinstance(inspect.getattr_static(class_info.cls, name), staticmethod)
 
     @staticmethod
     def is_abstract_method(obj: object) -> bool:
@@ -761,7 +763,7 @@ def generate_class_stub(self, class_name: str, cls: type, output: list[str]) ->
         The result lines will be appended to 'output'. If necessary, any
         required names will be added to 'imports'.
         """
-        raw_lookup = getattr(cls, "__dict__")  # noqa: B009
+        raw_lookup: Mapping[str, Any] = getattr(cls, "__dict__")  # noqa: B009
         items = self.get_members(cls)
         if self.resort_members:
             items = sorted(items, key=lambda x: method_name_sort_key(x[0]))
@@ -793,7 +795,9 @@ def generate_class_stub(self, class_name: str, cls: type, output: list[str]) ->
                         continue
                     attr = "__init__"
                 # FIXME: make this nicer
-                if self.is_classmethod(class_info, attr, value):
+                if self.is_staticmethod(class_info, attr, value):
+                    class_info.self_var = ""
+                elif self.is_classmethod(class_info, attr, value):
                     class_info.self_var = "cls"
                 else:
                     class_info.self_var = "self"


Test patch: