Search code examples
pythondjangooopoverridingmonkeypatching

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


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


Solution

  • 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.render\n")
            return stream
    

    b.py

    import a
    
    class B(a.A):
        def render(self, stream):
            stream.write("starting in B, to call super\n")
            super().render(stream)
            stream.write("ending in B\n")
            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 super\n")
            super().render(stream)
            stream.write("ending in B\n")
            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.