2009-05-05

Applying Patches in Guerrilla Way

How many times did you submit patches that have never been approved? How many times did you need to patch Django for some third-party application to work? How many times did you bumped into the wall working on real-world projects just because of some bugfixes waiting for tests or nicer solution? In all those cases, you should not panic nor run into circles, because you can still apply the existing patches in guerrilla way (or so called monkey patching).

Guerrilla patches overwrite existing code not on a disk, but in memory at execution time. Everything is an object in Python. Numbers, strings, functions, methods, classes, instances, modules, etc. are objects. Each object might be conceived as an attribute of a module where it is defined. In addition, properties and methods of classes can be conceived as attributes of the class. The principle of guerrilla patching is loading a module or class which attributes need to be changed, and assigning new values to those attributes before the module or class is used. All that has to happen somewhere in the beginning of execution. Usually, I put or import guerrilla patches in models.py of some app, because all models are loaded in one of the first steps of execution. So if you need to change a full class (DjangoClass), you would import the module (djangomodule) where the class resides, create another class (NewDjangoClass) and assign it to the attribute of the module by the name of the original class (djangomodule.DjangoClass = NewDjangoClass). If you need to change just one method (django_method) of a class (DjangoClass), then you would import the class, write a new function which takes the object to which it will be assigned as the first argument (def new_django_method(self, *args, **kwargs)) and assigned it to the class by the name of the original function (DjangoClass.django_method = new_django_method).

Let's have a look at an example. You might need nested fieldsets for more organized grouping, suggested in the ticket #10590. That allows you to create fieldset definitions as items of the 'fields' value of a parent fieldset.

As you might see from the applied patch nested_fieldsets.diff, the main change of the patch is changing most of the code for classes Fieldset and Fieldline residing in django/contrib/admin/helpers.py, also changing the function flatten_fieldsets in django/contrib/admin/util.py, introducing a new template tag and finally modifying a template.

The file with guerrilla patches should import helpers, util, and all other modules that will be used in the overwritten objects. Browse through the code below and check the differences with the original patch to understand the concept better.


from django import forms
from django.contrib.admin import helpers
from django.contrib.admin import util
from django.contrib.admin import options
from django.utils.safestring import mark_safe
from django.forms.formsets import all_valid
from django.conf import settings

### Guerilla patches for nested fieldsets

def flatten_fieldsets(fieldsets):
"""Returns a list of field names from an admin fieldsets structure."""
field_names = []
for name, opts in fieldsets:
for field in opts['fields']:
if isinstance(field, (tuple, list)):
if len(field)==2 and isinstance(field[1], dict):
# it's a nested fieldset
field_names.extend(flatten_fieldsets((field,)))
else:
# it's a tuple of field names
field_names.extend(field)
else:
# it's a field name
field_names.append(field)
return field_names

options.flatten_fieldsets = util.flatten_fieldsets = flatten_fieldsets

class Fieldset(object):
is_fieldset = True
def __init__(self, form, name=None, fields=(), classes=(), description=None, level=0):
self.form = form
self.name, self.fields = name, fields
self.classes = u' '.join(classes)
self.description = description
self.level = level

def _media(self):
if 'collapse' in self.classes:
return forms.Media(js=['%sjs/admin/CollapsedFieldsets.js' % settings.ADMIN_MEDIA_PREFIX])
return forms.Media()
media = property(_media)

def __iter__(self):
for field in self.fields:
if (len(field)==2 and isinstance(field[1], dict)):
# nested fieldset
yield Fieldset(self.form,
name=field[0],
fields=field[1].get("fields", ()),
classes=field[1].get("classes", ()),
description=field[1].get("description", ()),
level=self.level + 1,
)
else:
# field name or a tuple of field names
yield helpers.Fieldline(self.form, field)

helpers.Fieldset = Fieldset

class InlineFieldset(Fieldset):
def __init__(self, formset, *args, **kwargs):
self.formset = formset
super(InlineFieldset, self).__init__(*args, **kwargs)

def __iter__(self):
fk = getattr(self.formset, "fk", None)
for field in self.fields:
if fk and fk.name == field:
continue
if (len(field)==2 and isinstance(field[1], dict)):
# nested fieldset
yield Fieldset(self.form,
name=field[0],
fields=field[1].get("fields", ()),
classes=field[1].get("classes", ()),
description=field[1].get("description", ()),
level=self.level + 1,
)
else:
# field name or a tuple of field names
yield helpers.Fieldline(self.form, field)

helpers.InlineFieldset = InlineFieldset



Further, the template can be overwritten putting your own template path before the django admin path in settings.TEMPLATE_DIRS. The missing template tag can be defined in your own app. Just don't forget to load the library in the template ({% load mytemplatetags %}). CSS might be overwritten by including a custom CSS file to the bottom of HEAD section in a modified admin/change_form.html template.

That's all what you need and you'll be controlling the situation without branching Django. You have full power, but don't overuse this technique as it might be difficult to manage the updates, especially when you are guerrilla-patching undocumented features or helping functions which change backwards-incompatibly in the future. Still guerrilla-patched code in a third-party app is better than a requirement to patch Django itself.

Guerrilla patching might also be used in a similar way to do some slight modification to other third party apps which are not scalable by design. For example, you can add additional fields or modify methods of third-party apps which will be updatable separately.

Tomorrow (Wednesday) evening I am leaving the wonderful Prague and EuroDjangoCon. If you are here as well and want to discuss some concepts which I introduced in this blog, that's the last chance to meet me live. :D