7

I am trying to make a generic mixin for model fields (as opposed to form fields), the init for the mixin takes named arguments. I am running into trouble instantiating the mixin with another class.

Here is the code

class MyMixin(object):
    def __init__(self, new_arg=None, *args, **kwargs):
        super(MyMixin, self).__init__(*args, **kwargs)
        print self.__class__, new_arg


class MyMixinCharField(MyMixin, models.CharField):
    pass

...

class MyMixinModelTest(models.Model):
    myfield = MyMixinCharField(max_length=512,new_arg="myarg")

Making the migration for this model produces the following output:

<class 'myapp.mixintest.fields.MyMixinCharField'> myarg 
<class 'myapp.mixintest.fields.MyMixinCharField'> None 
<class 'myapp.mixintest.fields.MyMixinCharField'> None 
Migrations for 'mixintest':   
   0001_initial.py:
        - Create model MyMixinModelTest

First, why is init running 3 times? Where does the kwarg 'new_arg' in the second two? How do I create a field mixin for django?

EDIT: As opposed to another question, this question asks about field mixins, the linked question refers to model mixins.

Community
  • 1
  • 1
jmerkow
  • 1,811
  • 3
  • 20
  • 35

2 Answers2

5

First, why is init running 3 times?

Although the models.py is only imported once, the Field objects created therein, such as...

myfield = MyMixinCharField(max_length=512, new_arg="myarg")

...are cloned several times, which involves calling the field constructor using the keyword args they were originally created with. You can use the traceback module to see where it's happening...

import traceback

class MyMixin(object):
    def __init__(self, new_arg=None, *args, **kwargs):
        super(MyMixin, self).__init__(*args, **kwargs)
        print self.__class__, new_arg
        traceback.print_stack()

...which shows the following several times in the output...

  File "django/db/migrations/state.py", line 393, in from_model
    fields.append((name, field.clone()))
  File "django/db/models/fields/__init__.py", line 464, in clone
    return self.__class__(*args, **kwargs)
  File "myproj/myapp/models.py", line 11, in __init__
    traceback.print_stack()

Where is the kwarg 'new_arg' in the second two?

When you originally called...

myfield = MyMixinCharField(max_length=512, new_arg="myarg")

..."myarg" is being passed in as the new_arg parameter to...

def __init__(self, new_arg=None, *args, **kwargs):

...but because you don't pass that parameter to the underlying Field constructor...

super(MyMixin, self).__init__(*args, **kwargs)

...it's not stored anywhere in the underlying Field object, so when the field is cloned, the new_arg parameter isn't passed to the constructor.

However, passing that option to the superclass constructor won't work, because the CharField doesn't support that keyword arg, so you'll get...

  File "myproj/myapp/models.py", line 29, in MyMixinModelTest
    myfield = MyMixinCharField(max_length=512, new_arg="myarg")
  File "myproj/myapp/models.py", line 25, in __init__
    super(MyMixinCharField, self).__init__(*args, **kwargs)
  File "django/db/models/fields/__init__.py", line 1072, in __init__
    super(CharField, self).__init__(*args, **kwargs)
TypeError: __init__() got an unexpected keyword argument 'new_arg'

How do I create a field mixin for django?

Because of this cloning behavior, if you want to add custom field options, you have to define a custom deconstruct() method so that Django can serialize your new option...

class MyMixin(object):
    def __init__(self, new_arg=None, *args, **kwargs):
        super(MyMixin, self).__init__(*args, **kwargs)
        self.new_arg = new_arg
        print self.__class__, new_arg

    def deconstruct(self):
        name, path, args, kwargs = super(MyMixin, self).deconstruct()
        kwargs['new_arg'] = self.new_arg
        return name, path, args, kwargs


class MyMixinCharField(MyMixin, models.CharField):
    pass


class MyMixinModelTest(models.Model):
    myfield = MyMixinCharField(max_length=512, new_arg="myarg")

...which outputs...

<class 'myapp.models.MyMixinCharField'> myarg
<class 'myapp.models.MyMixinCharField'> myarg
<class 'myapp.models.MyMixinCharField'> myarg
Aya
  • 39,884
  • 6
  • 55
  • 55
  • I didn't answer all of my questions in my answer, you did. So I will accept yours. – jmerkow Jul 02 '16 at 17:00
  • Quick question, in my answer I have MyMixin inherit from models.Field, Does this affect anything? – jmerkow Jul 02 '16 at 17:02
  • @jmerkow It's not a particularly quick question to answer definitively, due to the use of metaclasses in `Model`. If `MyMixin` subclasses `Field` directly, then in some circumstances, functions in `CharField` which override those defined in `Field` may be ignored, and it'll end up calling the wrong function. It's probably safer to avoid that possibility, and just have the `MyMixin` subclass `object`, which is what mixins should do anyway. – Aya Jul 02 '16 at 17:36
  • That answers my question. Do you know of any good docs on this subject? – jmerkow Jul 02 '16 at 17:45
  • @jmerkow Well, a quick search yields [this question](http://stackoverflow.com/q/533631/172176), which has a couple of links in the comments that might be of interest. Mixins are ultimately just a special case of "multiple inheritence", so you could try googling for that to see some of the pros and cons. See also: [Python's Super is nifty, but you can't use it](https://fuhm.net/super-harmful/) – Aya Jul 02 '16 at 17:53
1

So I figured it out after lots of tinkering and re-reading the django docs on custom model fields You need a deconstructor along with your init. Django fields need a deconstruct method to serialize.

The mixin should have this method as well:

class MyMixin(object):
def __init__(self, new_arg=None, *args, **kwargs):
    self.new_arg = new_arg
    super(MyMixin, self).__init__(*args, **kwargs)

def deconstruct(self):
    name, path, args, kwargs = super(MyMixin, self).deconstruct()
    if self.new_arg is not None:
        kwargs['new_arg'] = self.new_arg
    return name, path, args, kwargs
jmerkow
  • 1,811
  • 3
  • 20
  • 35