'why monkey patching breaks staticmethods

Below is a simplified version of our production code that gave us an unexpected result after monkey patching. We have 3 classes here inherited in a chain Base -> A -> B. Those classes execute the Action class, which in this example just prints the passed string.

class Action():
    """an action prints passed argument when executed"""
    
    def __init__(self, arg):
        self.arg = arg
    
    def run(self):
        print(self.arg)

#chain of classes Base -> A -> B
class Base():
    name="base"
    
    @classmethod
    def actions(cls):
        return [Action(cls.name)]
        
        
class A(Base):
    name="AAAA"
    

class B(A):
    name="BBBB"


#sanity check
A.actions()[0].run() #AAAA
B.actions()[0].run() #BBBB

this prints:

AAAA

BBBB

as expected

we needed to monkey patch the middle class (A) and that operation affected class B. I'm trying to wrap my head around why?

#extend the actions of class "A" with an extra one by monkey patching "actions" classmethod

@classmethod
def newActions(cls):
    return cls.old_actions() + [Action("foobar")] #retrieve actions from the old classmethod and extend it

A.old_actions = A.actions #store the old actions classmethod for later use
A.actions = newActions #monkey patch actions

#sanity check
A.actions()[0].run() #AAAA
B.actions()[0].run() #AAAA ???

after monkey patching, all actions refer to class A and I would love to understand why it is happening

solution:

the problem with the above is how the old_actions are being stored. they need to be rewrapped as a classmethod as well

A.old_actions = classmethod(A.actions.im_func)



Solution 1:[1]

Like regular methods, classmethods bind on access. When you do

A.old_actions = A.actions

evaluating A.actions gives you a bound method, not a classmethod.

This line sets A.old_actions to a bound method with cls bound to A. Accessing B.old_actions will run the method with cls still bound to A rather than B.


When you call B.actions(), that calls your monkey-patched newActions with cls set to B, but newActions will call cls.old_actions(), which runs your original actions method with cls set to A, as described above.

Your original actions method then finds "AAAA" instead of "BBBB" when it accesses cls.name, because it's running with the wrong cls.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 user2357112