◐ Shell
clean mode source ↗

bpo-43682: Make staticmethod objects callable by vstinner · Pull Request #25117 · python/cpython

@vstinner

PR rebased to fix the enum doc fix from master.

@vstinner

I rebased my PR and reverted the io and_pyio changes (they should be made in a separated PR).

@vstinner

@serhiy-storchaka @gvanrossum @rhettinger: so, what do you think? this idea was rejected in 2015, but came back in 2021 and Guido likes the idea to make static methods callable. See also PR #25268 which makes staticmethod more "usable" to be used directly as a function.

It's a tricky topic. We could also require to always use staticmethod when using directly a function as a method:

class MyClass:
    method = staticmethod(func)

But sometimes, we need functions which can be used directly as method without staticmethod(), to mimick built-in function. Well see https://bugs.python.org/issue43682 and https://bugs.python.org/issue20309 discussions ;-)

@gvanrossum

I like the idea. Why was it rejected in 2015?

@serhiy-storchaka

staticmethod is a simple thing. It is only purpose to decorate functions before setting them as class attribute if we do not want to make them instance methods. There is no use cases for calling staticmethod object.

Victor want to replace OpenWrapper with staticmethod(open) to keep builtin open a non-descriptor just for the case if some user code sets it as class attribute. This is very special case, and I think that it would be better to apply staticmethod immediately before setting a class attribute:

class MyClass:
    open = staticmethod(open)

In any case staticmethod is not perfect replacement of OpenWrapper. If we go this way, we should at least implement __doc__ for staticmethod, and preferably __repr__, __name__, __module__, __qualname__, __text_signature__, etc, etc. It is a big issue.

@vstinner

In any case staticmethod is not perfect replacement of OpenWrapper. If we go this way, we should at least implement doc for staticmethod, and preferably repr, name, module, qualname, text_signature, etc, etc. It is a big issue.

I solved this in PR #25268, did you see my PR?

This is very special case

See also https://bugs.python.org/issue43682#msg389907:

My usecase is to avoid any behavior difference between io.open and _pyio.open functions: PEP 399 "Pure Python/C Accelerator Module Compatibility Requirements". Currently, this is a very subtle difference when it's used to define a method.

It's a similar issue than PEP 570 (positional-only arguments) solved for Python re-implementation of a C extension. We should either prevent C extensions to behave than Python, or we should allow pure Python code to behave the same.

Here the issue is complex (changing built-in functions or Python functions to add/remove descriptor), and I propose to only change staticmethod(). In the whole stdlib, I'm only aware of io.open which is used sometimes directly to define a method (without @staticmethod). But the issue happens with any built-in function whch is reimplemented in Python (e.g. in PyPy).

@vstinner

I merged my PR #25268 and rebased this PR on top of it.

By the way, just for consistency, should we also make class methods callable? I have no use case for that :-)

@gvanrossum

By the way, just for consistency, should we also make class methods

callable? I have no use case for that :-) I thought those are callable already.

@vstinner

Guido:

I thought those are callable already.

Me too, but hey, static methods and class methods are weird!

$ python3
Python 3.9.2 (default, Feb 20 2021, 00:00:00) 
>>> def func(): pass
... 
>>> wrapper = classmethod(func)
>>> wrapper()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'classmethod' object is not callable

It's only callable when it goes throught the descriptor!

@vstinner

Well, ignore my idea of making class methods callable. It makes no sense since it doesn't pass the class in this case. Example:

def func(cls):
    print(cls)

class MyClass:
    method = classmethod(func)

MyClass.method()
MyClass().method()
MyClass.__dict__['method']()

The last call fails because it doesn't go trought the descriptor, and so the class method doesn't get the class argument.

Output:

vstinner@apu$ python3 x.py 
<class '__main__.MyClass'>
<class '__main__.MyClass'>
Traceback (most recent call last):
  File "/home/vstinner/python/master/x.py", line 9, in <module>
    MyClass.__dict__['method']()
TypeError: 'classmethod' object is not callable

@gvanrossum

Well, ignore my idea of making class methods callable. It makes no sense since it doesn't pass the class in this case.

You should pass that in explicitly in that case. I guess calling classmethod(f)(C) could do the same thing as classmethod(f).__func__(C). But yeah, this doesn't seem as important as making staticmethod(f) callable, because there's nothing complicated there.

gvanrossum

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But please first fix the comment about class methods being callable.

Comment on lines 99 to 100

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class methods aren't callable yet.

gvanrossum

Static methods (@staticmethod) are now callable as regular functions.

@vstinner

I fixed test_pydoc which shows a nice enhancement of this PR ;-)

         self.assertEqual(self._get_summary_lines(X.__dict__['sm']),
-                         'sm(...)\n'
+                         'sm(x, y)\n'
                          '    A static method\n')

With this PR, inspect.signature() works on a static method, and returns the same signature than the wrapped callable object.

$ ./python
Python 3.10.0a7+
>>> def func(x: int, y: int) -> float: pass
... 
>>> import inspect
>>> inspect.signature(func)
<Signature (x: int, y: int) -> float>

>>> wrapper=staticmethod(func)
>>> inspect.signature(wrapper)
<Signature (x: int, y: int) -> float>

On Python 3.9 (and in master without this change), inspect.signature(wrapper) fails with "TypeError: <staticmethod...> is not a callable object".

@vstinner vstinner deleted the callable_staticmethod branch

April 11, 2021 22:21

@vstinner

@vstinner

It is possible to wrap a static method into a new static method, staticmethod(staticmethod(func)) or put two @staticmethod decorators on the same function. It is inefficient, but I don't think that the staticmethod() constructor must return the first static method unchanged, since static methods are mutable.

Python 3.10.0a7+
>>> def func(): return 5
... 
>>> wrapper1 = staticmethod(func)
>>> wrapper2 = staticmethod(wrapper1)

>>> wrapper1
<staticmethod(<function func at 0x7fffea3071d0>)>
>>> wrapper2
<staticmethod(<staticmethod(<function func at 0x7fffea3071d0>)>)>

>>> wrapper2()
5

>>> wrapper2.x=1
>>> wrapper1.x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'staticmethod' object has no attribute 'x'

Moreover, I expect that most decorators have a similar issue. Maybe a linter or a debug check can warn on that, but I don't think that staticmethod() must raise an error or return the first (static method) wrapper unchanged.