DEV Community

Iuliia Volkova
Iuliia Volkova

Posted on • Updated on • Originally published at xnu-im.space

Little bit of 'monkey patching'

Monkey Patching - the way to change objects behaviour in runtime. The
way that you can use to customize some third-party logic specially for
your purposes.

This is a pretty small article with an illustration case where Monkey
Patching can be useful. One day I thought about how to avoid sending
common used variables to Jinja2 templates in each route. And in Django,
Flask and aiohttp-jinja2 and many others frameworks/libs for this exists
context_processor.

Let’s imagine - you have a Web App with Sanic and Server Side Rendering
Templates with Jinja2.  For example, you have a header with Menu, some
logo, title - any information that is used on each page of your app. You
want to define it in one place.

In my case I need this in the process of work on the Gino-Admin panel.
And I use Sanic & Sanic-Jinja2.

I have something like this on each route:

return jinja.render(
   "login.html", request, objects=cfg.models, url_prefix=cfg.URL_PREFIX,
)

return jinja.render(
   "db_drop.html",
   request,
   data=await count_elements_in_db(),
   objects=cfg.models,
   url_prefix=cfg.URL_PREFIX,
)

return jinja.render(
   "presets.html",
   request,
   presets_folder=cfg.presets_folder,
   presets=utils.get_presets()['presets'],
   objects=cfg.models,
   url_prefix=cfg.URL_PREFIX,
)
Enter fullscreen mode Exit fullscreen mode

And etc.

As you see each time I send  url_prefix=cfg.URL_PREFIX, prefix that
used as a base route for admin panel and objects=cfg.models - list of
models that we render in Admin panel for add/edit/delete.

I don’t like to duplicate code, also I want to add more customisation,
like set custom title for admin panel in config and other things in the
future. And sending all new common variables inside each route will be
painful. So I need a way to provide these variables only once.

Sanic-Jinja2 also has context_processor arg but I tried a lot of things
to send in it, and nothing works for me. I failed. Maybe I have the
wrong hands & head.

But I want this feature, so go on.

SanicJinja2.render

To render all templates, the Sanic-JInja2 package uses the main method
render(...)

If you never used Sanic-Jinja2 to add it to Sanic app looks like:

from sanic_jinja2 import SanicJinja2

jinja = SanicJinja2()
Enter fullscreen mode Exit fullscreen mode

And that's it, after in routes you use it like:

return jinja.render(
   "presets.html",
   request,
   presets_folder=cfg.presets_folder,
   presets=utils.get_presets()['presets'],
   objects=cfg.models,
   url_prefix=cfg.URL_PREFIX,
)
Enter fullscreen mode Exit fullscreen mode

Okay. Time for monkey patching.

Main idea

go inside the source code, check that part code you need re-write -
and patch with your own implementation that solves your pain.

What I need?  I need to patch each response before page render with
common context variables that are used in each template.

If we jump in  SanicJinja2 source code we will see, that

https://github.com/lixxu/sanic-jinja2/blob/master/sanic_jinja2/__init__.py#L143

I need to change small method:

def render(self, template, request, status=200, headers=None, **context):
    return html(
            self.render_string(template, request, **context),
            status=status,
            headers=headers,
        )
Enter fullscreen mode Exit fullscreen mode

So I will define my own method. Let’s call it ‘custom_render’. I will
just copy the paste base method - render and add into context all
variables that I need.

def custom_render(
self, template, request, status=200, headers=None, **context
):
   context["admin_panel_title"] = cfg.name
   context["objects"] = cfg.models
   context["url_prefix"] = cfg.route
   context["admin_panel_version"] = __version__
   context["round_number"] = cfg.round_number
   return html(
       self.render_string(template, request, **context),
       status=status,
       headers=headers,
   )
Enter fullscreen mode Exit fullscreen mode

And now we need to ‘patch’ our target class SanicJinja2:

SanicJinja2.render = custom_render

So all the next code that will use this class after the line with
patch will call our custom method logic as the render method of
SanicJinja2 class.

Beautiful freedom of Python. You have a lot of ways to solve your
issues.

Code samples in project repo:

https://github.com/xnuinside/gino-admin/blob/master/gino_admin/config.py#L20 

Top comments (0)