Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django TypeError("'%s' is an invalid keyword argument for this function")

Tags:

python

django

So I've seen similar questions about this error, and they all seem to be related to use cases where there are ManyToMany relationships. However, I'm getting this issue even though my model doesn't have M2M relationships, so I wanted to ask and see why this was happening.

Here is my model:

class Course(models.Model):
    name = models.CharField(max_length=64)
    credit = models.IntegerField
    notes = models.CharField(max_length=128)
    resources = models.TextField
    description = models.TextField
    topic = models.CharField(max_length=128)

Whenever I create a new instance of this model, I get the TypeError for the credit, resources, and description fields.

I instantiate it like so:

c = Course(
    name='some name',
    credit='8',
    notes='N/A',
    resources='no resources',
    description='N/A',
    topic='some topic'
)

However if I change the affected fields to models.IntegerField(max_length=8) and models.TextField(max_length=8), then the error goes away.

Why does this happen? I was under the impression that the max_length parameter was optional for TextField; I don't even know what it means in the context of an IntegerField. Can someone explain this behavior, and/or what I'm doing wrong?

like image 456
yiwei Avatar asked May 23 '16 22:05

yiwei


2 Answers

You are defining the fields as their classes' references, and not as an instance of those classes. It should be

class Course(models.Model):
    name = models.CharField(max_length=64)
    credit = models.IntegerField()
    notes = models.CharField(max_length=128)
    resources = models.TextField()
    description = models.TextField()
    topic = models.CharField(max_length=128)
like image 83
rafaelc Avatar answered Sep 25 '22 05:09

rafaelc


If anyone's interested I think what happens in the django.db.models.ModelBase metaclass is fairly interesting. Overly verbose for answering this question? Almost certainly, I was originally not going to post but perhaps it will help (assuming what I have is correct of course). I've always found it more useful to understand why something works rather than why it doesn't. I'm not going to attempt to try to explain every little part that would be a monumental task, and am sticking to what's (hopefully) directly relevant. ¯\_(ツ)_/¯

(Also, it's likely I made a few errors in places, I don't dive into source code very often, please do feel free to correct)

Let's create a random model:

class RandomModel(models.Model):

     field1 = models.TextField()

Let's see what happens behind the scenes for something this deceptively simple. For the more complicated stuff, it's turtles all the way down.

We have models.Model as the base class, but it has ModelBase as it's metaclass. And, ModelBase has a __new__ method:

The source for the __new__ method of this metaclass is fairly long. Here's a link to the contents of the file in django\db\models\base.py.

There's two other classes we'll be concerned with django/db/models/options.py -> class Option and django/db/models/fields/__init__.py -> class Field.

Were you to print dir(field1) or if you were to simply look inside __init__.py in /fields you would see the Field class and a method named contribute_to_class.

Remember the contribute_to_class method, more on this later.

Let's break down the relevant components of the __new__ method. If you've ever seen a metaclass then you should be familiar with the arguments to the new function.

If not then check this link out: What is a metaclass in Python

class ModelBase(type):

    def __new__(..., attrs):

         ....

attrs here is the dictionary that contains all methods, attributes, etc.

Stuff happens, then we have this line:

new_class.add_to_class('_meta', Options(meta, app_label))

Now, what is add to class? It is this function:

def add_to_class(cls, name, value):

    # We should call the contribute_to_class method only if it's bound
    if not inspect.isclass(value) and hasattr(value, 'contribute_to_class'):
        value.contribute_to_class(cls, name)
    else:
        setattr(cls, name, value)

We could stop right here, and I could have actually started with this function. This is the entire problem. You never initialize models.FieldName and thus it is not an instance that passes the condition hasattr(value, 'contribute_to_class'). Instead, the else statement is executed and it simply becomes a class attribute (which doesn't contribute). As the name suggests hasattr simply checks if the object has said attribute available (passed as a string to hasattr).

If you just did print(field1) with field1 = models.TextField you'd see that it was simply a reference to the class django.db.models.fields.TextField.

Getting back on track, let's keep going! What if it does pass this condition? Then, we have that add_to_class now calls contribute_to_class with the parameters cls and name.

But, what does contribute_to_class do?!

Here's the function contribute_to_class:

def contribute_to_class(self, cls, name, virtual_only=False):

   self.set_attributes_from_name(name)
   self.model = cls
   if virtual_only:
       cls._meta.add_field(self, virtual=True)
   else:
       cls._meta.add_field(self)
    if self.choices:
        setattr(cls, 'get_%s_display' % self.name,
                curry(cls._get_FIELD_display, field=self))

Really, what we care about here is cls._meta.add_field(self).

Now, we need to backtrack. At this point things are probably starting to get confusing, but hopefully all will be clear in a moment.

Recall the add_to_class line:

new_class.add_to_class('_meta', Options(meta, app_label))

Depending upon how long you've been using Django at some point it is very likely you've used the ._meta attribute more than likely like this _meta.fields.

Well, here it is!!

Now, recall (again) the add_to_class function the Options class instance has no method contribute_to_class and is thus added to the class as an attribute _meta.

I'm not going to paste the contribute_to_class function here. It's also a bit long-winded and is fairly well commented. You can find it here and it's about 1/4 of the way down. I'd recommend reading the comments and once you have the function should be more or less intuitive.

So, now there's this new attribute _meta (which is an Option class instance) that has the class method add_field available to it.

And, here's the add_field method:

 def add_field(self, field, virtual=False):
    # Insert the given field in the order in which it was created, using
    # the "creation_counter" attribute of the field.
    # Move many-to-many related fields from self.fields into
    # self.many_to_many.
    if virtual:
        self.virtual_fields.append(field)
    elif field.is_relation and field.many_to_many:
        self.local_many_to_many.insert(bisect(self.local_many_to_many, field), field)
    else:
        self.local_fields.insert(bisect(self.local_fields, field), field)
        self.setup_pk(field)

The comments are fairly self explanatory again. However, what might be confusing is the line bisect(list, thing_in_list). Here, bisect is simply short for bisect.bisect_right from the python module bisect. As the comments says the fields are inserted in the order in which they are created, which is a sorted order. Using bisect allows the list to retain sorted order while inserting, it locates the proper insertion index.

What's next in the __new__ method of the ModelBase metaclass?

We have the following line:

 # Add all attributes to the class.
 for obj_name, obj in attrs.items():
      new_class.add_to_class(obj_name, obj)

Now, recall that attrs is the dict containing well everything. So, it's looping through the object names (keys) and the objects (values) of this dictionary. And, as discussed earlier looking for instances with the contribute_to_class method otherwise it is simply added as an attribute to the class instance (the new class to be created remember __new__ is actually creating the class here). And, that process sort of repeats.

Then, the next two lines:

    new_fields = chain(
        new_class._meta.local_fields,
        new_class._meta.local_many_to_many,
        new_class._meta.virtual_fields
    )
    field_names = {f.name for f in new_fields}

Here, itertools.chain is flatting these lists into a single list and returns a chain object.

This chain object is then iterated over calling the attribute name from the Field class (remember the fields are actually instances of (subclasses) Field) into a set and a set is simply a collection of objects without duplicates.

More things happen, but I'm just going to end here as I think everything else is fairly redundant at this point for this example.

like image 31
Pythonista Avatar answered Sep 23 '22 05:09

Pythonista