Search code examples
pythondjangoattributeerror

Defining a class object with type() : Why can't I use the name of an attribute meant to be deepcopied?


For some reasons I had to create a form class in Django by using the type() function.

The return statement of my function generator (makeFurnitureForm()) looked like this :

    return (type("FurnitureForm",
                 (forms.BaseForm, form_utils.forms.BetterBaseForm,),
                 {"_fieldsets" : dic, "base_fields" : fields,
                  "_fieldset_collection" : None, '_row_attrs' : {}}))

The class object I create inherits from two other classes. Here is the constructor of one of them : form_utils.forms.BetterBaseForm

    def __init__(self, *args, **kwargs):
        self._fieldsets = deepcopy(self.base_fieldsets)
        self._row_attrs = deepcopy(self.base_row_attrs)
        self._fieldset_collection = None
        super(BetterBaseForm, self).__init__(*args, **kwargs)

Everything works fine with my code, but on a previous version, I used a code like this :

    return (type("FurnitureForm",
                 (forms.BaseForm, form_utils.forms.BetterBaseForm,),
                 {"base_fieldsets" : dic, "base_fields" : fields}))

Since _fieldsets and _fieldset_collection are explicitly defined and set in the BetterBaseForm constructor by deepcopying base_fieldsets attribute and assigning None value, I didn't understand why I got AttributeErrors when trying to access _fieldsets and _fieldset_collection attribute of an instance (when rendering my django page for instance).

Here are my questions :

  1. Doesn't type() calls base classes` constructors, and don't these constructors define and set attributes properly?

  2. Why did I have to explicitly set these attributes by myself?

  3. Why didn't I got an AttributeError when the constructor tries to access an attribute (row_attrs for instance) during the type() call? (I got one when trying to access it later though).

I use my makeFurnitureForm() like this in my django view :

return (render(request,'main/devis-bis.html', {"form" : (makeFurnitureForm())()}))

The class object is directly passed to a view, and then to a template to be rendered.

EDIT : Traceback and code.

Example with this return :

    return (type("FurnitureForm",
                 (forms.BaseForm, form_utils.forms.BetterBaseForm,),
                 {"base_fieldsets" : dic, "base_fields" : fields,
                  "_fieldset_collection" : None, '_row_attrs' : {}}))

I also found out I directly pass a class object to the render() function, I don't understand why it even works because it expects an instance. I corrected it in the render() call.

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/forms/forms.py" in __getitem__
  141.             field = self.fields[name]

During handling of the above exception ('fieldsets'), another exception occurred:

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/template/base.py" in _resolve_lookup
  883.                     current = current[bit]

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/forms/forms.py" in __getitem__
  144.                 "Key %r not found in '%s'" % (name, self.__class__.__name__))

During handling of the above exception ("Key 'fieldsets' not found in 'FurnitureForm'"), another exception occurred:

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/core/handlers/base.py" in get_response
  149.                     response = self.process_exception_by_middleware(e, request)

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/core/handlers/base.py" in get_response
  147.                     response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "/nfs/2013/v/vmonteco/Code/web/demenhouse/htdocs/main/views.py" in secondform
  69.         return (render(request,'main/devis-bis.html', {"form" : makeFurnitureForm()}))

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/shortcuts.py" in render
  67.             template_name, context, request=request, using=using)

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/template/loader.py" in render_to_string
  97.         return template.render(context, request)

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/template/backends/django.py" in render
  95.             return self.template.render(context)

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/template/base.py" in render
  206.                     return self._render(context)

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/template/base.py" in _render
  197.         return self.nodelist.render(context)

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/template/base.py" in render
  992.                 bit = node.render_annotated(context)

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/template/base.py" in render_annotated
  959.             return self.render(context)

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/template/loader_tags.py" in render
  173.         return compiled_parent._render(context)

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/template/base.py" in _render
  197.         return self.nodelist.render(context)

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/template/base.py" in render
  992.                 bit = node.render_annotated(context)

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/template/base.py" in render_annotated
  959.             return self.render(context)

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/template/loader_tags.py" in render
  69.                 result = block.nodelist.render(context)

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/template/base.py" in render
  992.                 bit = node.render_annotated(context)

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/template/base.py" in render_annotated
  959.             return self.render(context)

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/template/defaulttags.py" in render
  161.                 values = self.sequence.resolve(context, True)

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/template/base.py" in resolve
  709.                 obj = self.var.resolve(context)

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/template/base.py" in resolve
  850.             value = self._resolve_lookup(context)

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/django/template/base.py" in _resolve_lookup
  891.                         current = getattr(current, bit)

File "/nfs/2013/v/vmonteco/Envs/demenhouse/lib/python3.5/site-packages/form_utils/forms.py" in fieldsets
  253.                 self, self._fieldsets)

Exception Type: AttributeError at /devisbis/1679091c5a880faf6fb5e6087eb1b2dc/
Exception Value: 'FurnitureForm' object has no attribute '_fieldsets'

Solution

    1. Doesn't type() calls base classes` constructors, and don't these constructors define and set attributes properly?

      You are looking at initialisers, not constructors, hence the name __init__. These are automatically called when an instance is created. type() doesn't call these, because you are not producing an instance, you are producing a class.

      Once you call the resulting class to create an instance, then the __init__ method is called.

    2. Why did I have to explicitly set these attributes by myself?

      I don't know, you don't show how you are creating an instance, or any other code showing your error. If you are expecting those attributes to exist on the class, then your assumption there is wrong.

    3. Why didn't I got an AttributeError when the constructor tries to access an attribute (row_attrs for instance) during the type() call? (I got one when trying to access it later though).

      Again, type() produces a new class, not an instance, and __init__ is not called at this time.

    You should not need to add those attributes to your class namespace if the __init__ method will add them to your instance namespace.

    I must note that the forms.BaseForm.__init__ method is not designed to cooperate with other classes coming later in the MRO; it never calls super().__init__(). If you do create an instance of your new class, the form_utils.forms.BetterBaseForm.__init__() method will not be called, unless you were to swap the order of your two base classes:

    return type(
        "FurnitureForm", (form_utils.forms.BetterBaseForm, forms.BaseForm,),
        {"base_fieldsets" : dic, "base_fields" : fields})
    

    That's because with BetterBaseForm listed first, it'll have it's __init__ be called, and the super(BetterBaseForm, self).__init__(*args, **kwargs) line in that method will then call BaseForm.__init__().

    Reading your traceback I do note that you never create an instance. You should always create an instance of a form class, and optionally pass in request.POST to pre-populate the fields and validate the form input.

    So instead of:

    return (render(request,'main/devis-bis.html', {"form" : makeFurnitureForm()}))
    

    use

    return render(request,'main/devis-bis.html', {"form" : makeFurnitureForm()()})
    

    Note the extra (). You probably want to check if the form is valid at some point, however, so you'd use this:

    form_class = makeFurnitureForm()
    if request.method == 'POST':
        form = form_class(request.POST)
        if form.is_valid():
            # ...
            # process data, redirect somewhere
            return HttpResponseRedirect('/thanks/')
    
    else:
        form = form_class()
    
    return render(request,'main/devis-bis.html', {"form" : form})