0

I have a Django model say:

class MyOperation(models.Model):
    name = models.CharField(max_length=100)

    def my_method(self):
        pass

I want to be able to add, from a different Django app, a new method to this class.

I tried to do this from a different app module called custom.py:

from operations.models import MyOperation

def op_custom(test):
    print "testing custom methods"

MyOperation.op_custom = op_custom

However this does not seems to work, in runtime the method does not exists.

Is this the way to modify the definition of a class from a different django application? Is there a better way to do it?

Thanks

Jose Luis de la Rosa
  • 696
  • 1
  • 10
  • 14
  • 1. Don't do this. 2. Don't ever do this. 3. This breaks introspection and messes with human parsing of python code so it should not be done. 4. Don't. 5. If you absolutely have to do this, you can, as demonstrated by @knbk's link. – astex Feb 18 '15 at 18:55
  • It is not the same question, I am asking how to add the method, in a Django Model, I mean, the class, not the object. – Jose Luis de la Rosa Feb 18 '15 at 18:56
  • @JoseLuisdelaRosa That's exactly what the link answers, unless I misunderstand what you're actually asking. One question - are you actually running the code in `custom.py`? – knbk Feb 18 '15 at 19:19
  • @astex What kind of introspection doesn't pick up the new methods? The `inspect` module seems to work fine. Tbh I think it's sometimes unavoidable with Django models. You can't simply subclass third-party models without side effects. – knbk Feb 18 '15 at 19:26
  • `inspect` picks them up, but they don't come from anywhere in the `mro()`. Some python libraries (e.g flask-classy or my own [private] project foodtoeat.com) use a convoluted system involving iterating through the mro() and grabbing the argspecs of methods to correctly route to a method within the class. In these cases, the monkey-patched method would be missed entirely. – astex Feb 18 '15 at 20:15
  • While it's true that you cannot subclass third-party models in a django application (due to the use of meta classes), you _can_ add another layer of metaclasses and a [proxy class](http://code.activestate.com/recipes/496741-object-proxying/) to do this in a more reasonable-looking way. I'm not sure if it would actually simplify the implementation at all or if it would just help organize the code. Probably better to just use functions that take the object as an argument. – astex Feb 18 '15 at 20:17
  • It's worth noting that in addition to the steps linked by @knbk, you should use the wraps decorator to maintain metadata: `MyOperation.my_method = wraps(MyOperation.my_method)(op_custom)` – DylanYoung Aug 26 '16 at 17:02

1 Answers1

4

The Right Way™ to do this is via a mixin:

class MyMixIn(object):
    def op_custom(self):
        print("foo")

class MyOperation(models.Model, MyMixIn):
    def op_normal(self):
        print("bar")

This has several advantages over monkey-patching in another method:

  • The mixin is in the class' method resolution order, so introspection works perfectly.
  • It is guaranteed to be defined on every instance of the class regardless of where it is instantiated since the class itself inherits from the mixin. Otherwise, you may end up with instances from before the monkey patch is applied where this method isn't there.
  • It provides an obvious place to add such methods in the future.
Community
  • 1
  • 1
astex
  • 1,045
  • 10
  • 28
  • Shouldn't the model's inheritance be reversed, e.g. the MyMixIn should come before he models.Model? `class MyOperation(MyMixIn, models.Model)` – Tim Feb 19 '15 at 09:51
  • 1
    @TimSchneider It depends on the intended use. If the mixin should override methods from the `Model` class, then yes. However, some cases will require the opposite. For example, the mixin could be used to 'spoof' the model class by writing custom `save()`, ... methods while providing additional functionality that can be used with a real model by inheriting in the way above. – astex Feb 19 '15 at 20:02
  • This neither answers the question nor serves the same purpose. Adding the Mixin to the `__bases__` of the original instance might improve the answer. – DylanYoung Aug 26 '16 at 18:09