Skip to content

bpo-43682: Make staticmethod objects callable #25117

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 11, 2021
Merged

bpo-43682: Make staticmethod objects callable #25117

merged 2 commits into from
Apr 11, 2021

Conversation

vstinner
Copy link
Member

@vstinner vstinner commented Mar 31, 2021

Static methods created by the @staticmethod decorator are now
callable as regular functions.

https://bugs.python.org/issue43682

@vstinner
Copy link
Member Author

vstinner commented Apr 2, 2021

PR rebased to fix the enum doc fix from master.

@vstinner
Copy link
Member Author

vstinner commented Apr 7, 2021

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

@vstinner
Copy link
Member Author

vstinner commented Apr 8, 2021

@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
Copy link
Member

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

@serhiy-storchaka
Copy link
Member

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
Copy link
Member Author

vstinner commented Apr 9, 2021

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
Copy link
Member Author

vstinner commented Apr 9, 2021

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
Copy link
Member

gvanrossum commented Apr 9, 2021 via email

@vstinner
Copy link
Member Author

vstinner commented Apr 9, 2021

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
Copy link
Member Author

vstinner commented Apr 9, 2021

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
Copy link
Member

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.

Copy link
Member

@gvanrossum gvanrossum left a comment

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
# bpo-43682: Static methods and class methods are callable
# since Python 3.10
Copy link
Member

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.

Static methods (@staticmethod) are now callable as regular functions.
@vstinner
Copy link
Member Author

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 merged commit 553ee27 into python:master Apr 11, 2021
@vstinner vstinner deleted the callable_staticmethod branch April 11, 2021 22:21
@vstinner
Copy link
Member Author

Thanks for the review @gvanrossum.

@vstinner
Copy link
Member Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants