20

I came upon this post on monkey patching Django:

from django.contrib.auth.models import User

User.add_to_class('openid', models.CharField(max_length=250,blank=True))

def get_user_name(self):
    if self.first_name or self.last_name:
        return self.first_name + " " + self.last_name
    return self.username

User.add_to_class("get_user_name",get_user_name)

I understand that this isn't ideal and it's better to add fields and functions to User through a separate model Profile.

With that said, I just want to understand how this would work:

  1. Where would I put the monkey patching code?

  2. When is the code run -- just once? once per Python interpreter startup? once per request?

  3. Presumably I'd still need to change the DB schema. So if I dropped the table User and ran ./manage.py syncdb, would syncdb "know" that a new field has been added to User? If not how do I change the schema?

Continuation
  • 12,722
  • 20
  • 82
  • 106

3 Answers3

20

put the file monkey_patching.py in any of your apps and import it in app's __init__.py file. ie:

app/monkey_patching.py

#app/monkey_patching.py
from django.contrib.auth.models import User

User.add_to_class('openid', models.CharField(max_length=250,blank=True))

def get_user_name(self):
    if self.first_name or self.last_name:
        return self.first_name + " " + self.last_name
    return self.username

User.add_to_class("get_user_name",get_user_name)

app/__init__.py

#app/__init__.py
import monkey_patching
suhailvs
  • 20,182
  • 14
  • 100
  • 98
  • 4
    does not work with django 1.11. I get django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet. – sureshvv Apr 27 '18 at 06:03
  • @sureshvv, You can add it to an `models.py` and placed the import of that app above the patched app in settings.INSTALLED_APPS. However there is still a potential mess with migrations... – maersu Oct 16 '18 at 19:22
9

You could put it anywhere, but it's common to see this kind of stuff linked in the settings file (or even the urlconf). Anywhere you could put a signal might also be appropriate. This code should really be slightly more intelligent - often files get imported more than once and there's not a lot you can do about it, so you can run into problems if you try to run code like this multiple times.

The code needs to be executed at least once for each python process.

Yes you would need to change the DB by hand. Syncdb probably wouldn't catch the change (I haven't looked closely at the code), but there might be some places you could put the code that would work.

You seem to already know that this is a terrible, horrible thing to do and should never be done for real code, so I won't belabor that point. Doing this kind of thing is a fantastic way to generate really difficult to find bugs in your code, in addition to code that may not work in future versions of Django.

Also, it won't work well with South, which you should be using.

Paul McMillan
  • 19,693
  • 9
  • 57
  • 71
  • Thanks. So settings and urlconf files get run once (and only once?) on each Python process initialization? I'm always a little unsure about when and how often those files are run. – Continuation Jul 17 '11 at 00:05
  • No, they don't necessarily. They just don't usually get run many times. You CANNOT rely on any file at all being run only once. This is why signal handlers have the parameter used for uniqueness. – Paul McMillan Jul 17 '11 at 00:07
  • I see. I'm not familiar with signal handlers and the uniqueness parameter you mentioned. Could you explain? Or is there somewhere I can read more about it? – Continuation Jul 17 '11 at 00:26
  • Check out the docs on signal dispatchers. The parameter I was referring to is `dispatch_uid`. https://docs.djangoproject.com/en/1.3/topics/signals/#listening-to-signals – Paul McMillan Jul 17 '11 at 23:58
2

Using both @suhailvs and @Paul McMillan's suggestions I added a patch.py file to the root of my app, and inside the app's apps.py called my patched in the ready signal:

from django.apps import AppConfig

class MyAppConfig(AppConfig):
    name = "MyApp"

    def ready(self):
        """
        Called when the app is ready.
        """
        from .patch import patch_func

        patch_func()

The AppRegistryNotReady: Apps aren't loaded yet. happens because of the import, not the function call, so it must be inside the function.

felipe
  • 7,324
  • 2
  • 28
  • 37