Overriding method python class of a pip module to update its behavior globally

Question:

Using OOP I want to do something like

from django.contrib import admin

class NavigateFormAdmin(admin.ModelAdmin):
    def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
        context['next_record_id'] = custom_function_to_calculate(context['obj'].id)
        res = super().render_change_form(request, context, add, change, form_url)
        return res

And expect that whenever render_change_form of admin.ModelAdmin is called, it should first my overridden method (above) which then should call the original (parent) method. but it makes no difference, because my overridden method never gets called rather on any call to render_change_form the method from original class admin.ModelAdmin is called.


Using undesired monkey patching

I am able to achieve what I need by adding following code to any of my py file that is read by interpreter at the start of my project/service execution

from django.contrib import admin
from django.template.response import TemplateResponse
# and also all the other imports used in original medthod

class NavigateFormAdmin(admin.ModelAdmin):
    def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
        opts = self.model._meta
        app_label = opts.app_label
        preserved_filters = self.get_preserved_filters(request)

        # and all the code of existing function has to be repeated here

        context['next_record_id'] = custom_function_to_calculate(context['obj'].id)

        res = TemplateResponse(request, form_template, context)
        return res

admin.ModelAdmin.render_change_form = NavigateFormAdmin.render_change_form

Now on every call to admin.ModelAdmin.render_change_form off-course NavigateFormAdmin.render_change_form is executed

But I need to use super() like (the first piece of code which is not working) here because OOP means re-usability, so what I could achieve is not satisfactory as all the 50 lines code of original method is repeated to only one line for overriding. Also this repeated code cause some unexpected results for changed version of admin.ModelAdmin

Asked By: Sami

||

Answers:

You can use super() and monkey patch like you expected, though you can’t just monkey patch the individual method, but you must patch out the entire object with your new subclass. Since you didn’t include the errors you have encountered, I assume you may have saw a TypeError exception about super.

For a minimum demonstration, we need the two modules as follows:

a.py

class A:
    def render(self, stream):
        stream.write("calling A.rendern")
        return stream

b.py

import a

class B(a.A):
    def render(self, stream):
        stream.write("starting in B, to call supern")
        super().render(stream)
        stream.write("ending in Bn")
        return stream

# monkey patching a.A with the new B
a.A = B

As a demonstration:

>>> from io import StringIO
>>> import a
>>> print(a.A().render(StringIO()).getvalue())
calling A.render

That worked as normal, now if b were to be imported, the monkey patch will apply:

>>> import b
>>> print(a.A().render(StringIO()).getvalue())
starting in B, to call super
calling A.render
ending in B

Note how the subclass B can simply user super() to reference its parent class, even when it is no longer assigned to a.A – the underlying class hierarchy is maintained, and super() inside B will do the right thing.

However, in your actual monkey patch example, you tried to rebind a specific method – this will in fact not work because re-assignment of a function inside a class block will result the new function being part of the class definition. Effectively, a reassignment in the form of the following

a.A.render = B.render

Will effectively result in the A class looking something like the following definition (it’s not actually, the details are significantly much more complicated than this illustration, but does roughly illustrates the problem you may have faced):

class A:
    def render(self, stream):
        stream.write("starting in B, to call supern")
        super().render(stream)
        stream.write("ending in Bn")
        return stream

Given that A does not have subclass from anything, it can’t call super() like that and calling A().render(...) will result in an exception (the details depends on how the render method is actually defined and then assigned to the class).

So in short, you may use the original code that you wanted to do, and monkey patch the whole class, i.e. by doing the following:

admin.ModelAdmin = NavigateFormAdmin

Do note that monkey patching can have their own pitfalls if your changes become incompatible with the underlying expectations of the original packages and their dependencies/dependents.

Answered By: metatoaster