python / cpython

The Python programming language
https://www.python.org
Other
63.72k stars 30.53k forks source link

Behaviour change in partialmethod in Python3.11 #99152

Open garyvdm opened 2 years ago

garyvdm commented 2 years ago

Bug report

The behavior of partialmethod changed in python3.11. This caused a library that I use testsuite to fail.

Here is a minimal use case that demonstrates this:

from functools import partialmethod

class Cell:
    def __init__(self):
        self.alive = False

    def set_state(self, state):
        self.alive = bool(state)

class Wrapper:
    def set_state_wrap(self, cell: Cell, state):
        cell.set_state(state)

wrapper = Wrapper()

Cell.set_alive = partialmethod(wrapper.set_state_wrap, True)

cell = Cell()
cell.set_alive()
print(cell.alive)

In python3.10, this prints True. In python3.11 it errors with:

Traceback (most recent call last):
  File "/home/gary/partialmethod.py", line 23, in <module>
    c.set_alive()
TypeError: Wrapper.set_state_wrap() missing 1 required positional argument: 'state'

The arguments passed to Wrapper.set_state_wrap are: python3.10: wrapper, cell, True python3.11: cell, True

So partialmethod is forgetting that set_state_wrap is a method of wrapper, and it needs to pass that in as the first arg.

I know that the way that they are using partialmethod here is not how it is intended to be used, and feels wrong. However this change is behavior if intended should be documented.

Your environment

sweeneyde commented 2 years ago

I bisected to here:

a918589578a2a807396c5f6afab7b59ab692c642 is the first bad commit commit a918589578a2a807396c5f6afab7b59ab692c642 Author: Michael J. Sullivan sully@msully.net Date: Wed May 4 21:00:21 2022 -0700

bpo-46764: Fix wrapping bound method with @classmethod (#31367)
sweeneyde commented 2 years ago

cc @rhettinger

pgcd commented 1 year ago

Having stumbled across this in a Django app where I was using Field.contribute_to_class to add some extra functionality to models that I couldn't change directly, I experimented with a few different workarounds with very little success. Eventually I decided the easiest solution was an explicit decorator, and this is what I'm using now (in the very specific cases where I need to ensure the old behavior is maintained:

class partialmethod_with_self(partialmethod):
    def __get__(self, obj, cls=None):
        return self._make_unbound_method().__get__(obj, cls)

This works in my case, and it should be backwards-compatible as well (I tested against 3.11 and 3.9.2).

goffioul commented 2 months ago

Is this behavior change documented anywhere? Will it ever be fixed? Is it considered as a bug at all by the python devs?

I am also being hit by this problem in the context of django (and the contribute_to_class pattern). This basically boils down to this snippet (works fine until 3.10, errors out in 3.11 and 3.12):

from functools import partialmethod

class A:
    @classmethod
    def f1(cls, self, value):
        self.value = value

class B:
    def __init__(self):
        self.value = None

setattr(B, 'set_value_true', partialmethod(A.f1, True))

b = B()
b.set_value_true()
print(b.value)