Trouble trying to dynamically add methods to Python class (i.e. django-tables2 'Table')
Asked Answered
F

2

8

So for a Django project, I would really like to be able to generate and display tables (not based on querysets) dynamically without needing to know the contents or schema beforehand.

It looks like the django-tables2 app provides nice functionality for rendering tables, but it requires that you either explicitly declare column names by declaring attributes on a custom-defined Table subclass or else provide a model for it infer the columns.

I.e, to use a column named "name", you'd do:

class NameTable(tables.Table):
   name = tables.Column()

The Tables class does not provide a method for adding columns post-facto because, from reading the source, it seems to use a metaclass that sweeps the class attributes on __new__ and locks them in.

It seemed like very simple metaprogramming would be an elegant solution. I defined a basic class factory that accepts column names are arguments:

def define_table(columns):
    class klass(tables.Table): pass              
    for col in columns:
        setattr(klass, col, tables.Column())
    return klass

Sadly this does not work. If I run `

x = define_table(["foo", "bar"])(data)
x.foo
x.bar

I get back:

<django_tables2.columns.base.Column object at 0x7f34755af5d0>
<django_tables2.columns.base.Column object at 0x7f347577f750>

But if I list the columns:

print x.base_columns

I get back nothing i.e. {}

I realize that there are probably simpler solutions (e.g. just bite the bullet and define every possible data configuration in code, or don't use django-tables2 and roll my own), but I am now treating this as an opportunity to learn more about meta programming, so I would really like to make this work this way.

Any idea what I'm wrong doing wrong? My theory is that the __new__ method (which is redefined in the metaclass Table uses) is getting invoked when klass is defined rather than when it's instantiated, so by the time I tack on the attributes it's too late. But that violates my understanding of when __new__ should happen. Otherwise, I'm struggling to understand how the metaclass __new__ can tell the difference between defined-in-code attributes vs. dynamically defined ones.

Thanks!

Foliated answered 3/1, 2013 at 7:8 Comment(0)
L
6

You're on the right track here, but instead of creating a barebones class and adding attributes to it, you should use the type() built-in function. The reason it's not working the way you're trying, is because the metaclass has already done its work.

Using type() allows you to construct a new class with your own attributes, while setting the base class. Meaning - you get to describe the fields you want as a blueprint to your class, allowing the Tables metaclass to take over after your definition.

Here's an example of using type() with django. I've used this myself for my own project (with some slight variations) but it should give you a nice place to start from, considering you're already almost there.

def define_table(columns):
    attrs = dict((c, tables.Column()) for c in columns)
    klass = type('DynamicTable', (tables.Table,), attrs)
    return klass
Leandra answered 3/1, 2013 at 7:15 Comment(2)
Awesome. This works! Alternatively, it seems that just doing table.base_columns[col] = col works, but that may be breaking something I'm not seeing since it circumvents the column creation code. So your solution is much better. Future readers should also check out @BrenBarn's answer since it explains why my method was failing.Foliated
@Foliated glad it worked for you. While it's in your mind, you should have a read about metaclasses, and the various functions they provide. They're especially useful to understand when playing around with Django, as Models and Forms both use them, and sometime you're going to have to get into the nitty-gritty.Leandra
M
1

You're confusing the __new__ of a "regular" class with the __new__ of a metaclass. As you note, Table relies on __new__ method on its metaclass. The metaclass is indeed invoked when the class is defined. The class is itself an instance of the metaclass, so defining the class is instantiating the metaclass. (In this case, Table is an instance of DeclarativeColumnMetaClass.) So by the time the class is define, it's too late.

One possible solution is to write a Table subclass that has some method refreshColumns or the like. You could adapt the code from DeclarativeColumnMetaclass.__new__ to essentially make refreshColumns do the same magic again. Then you could call refreshColumns() on your new class.

Marquismarquisate answered 3/1, 2013 at 7:16 Comment(1)
Thanks for answering! I think Josh's answer is a bit more elegant, but your answer helps explain why my method failed.Foliated

© 2022 - 2024 — McMap. All rights reserved.