views:

51

answers:

3

I'm using SQLAlchemy, and many classes in my object model have the same two attributes: id and (integer & primary key), and name (a string). I'm trying to avoid declaring them in every class like so:

class C1(declarative_base()):
    id = Column(Integer, primary_key = True)
    name = Column(String)
    #...

class C2(declarative_base()):
    id = Column(Integer, primary_key = True)
    name = Column(String)
    #...

What's a good way to do that? I tried using metaclasses but it didn't work yet.

+1  A: 

Use a class decorator: All the goodness[*], and less fuss than a metaclass:

def id_name(cls):
    cls.id = Column(Integer, primary_key = True)
    cls.name = Column(String)
    return cls

@id_name
class C2():
    pass

print(C2.id)
# Column(Integer, primary_key = True)

[*] Class decorators can not do everything that a metaclass could do, but for adding class attributes it is just fine.

unutbu
ahhh, it works great for general attributes - thank you!.. but it seems that SQLAlchemy doesn't perform the instrumentation properly when I try it. If I try this for `__tablename__`, it complains that the class does not have `__tablename__` defined; and if I try it for `id`, it complains it can't find the primary key...
max
Hm. Sorry. I don't think I understand SQLAlchemy well enough to help you here.
unutbu
I suspect what happens here is that SQLAlchemy does its instrumentation while the class is being created. Any later addition of attributes (e.g., with decorators here) comes too late: by then all the instrumentation is already completed.
max
+1  A: 

You could factor out your common attributes into a mixin class, and multiply inherit it alongside declarative_base():

from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

class IdNameMixin(object):
    id = Column(Integer, primary_key=True)
    name = Column(String)

class C1(declarative_base(), IdNameMixin):
    __tablename__ = 'C1'

class C2(declarative_base(), IdNameMixin):
    __tablename__ = 'C2'

print C1.__dict__['id'] is C2.__dict__['id']
print C1.__dict__['name'] is C2.__dict__['name']

EDIT: You might think this would result in C1 and C2 sharing the same Column objects, but as noted in the SQLAlchemy docs, Column objects are copied when originating from a mixin class. I've updated the code sample to demonstrate this behavior.

dhaffey
Unfortunately, this is not going to work because the id attribute would then be shared among all the subclasses of IdNameMixin. In SQLAlchemy, each class must have its own id (a newly created object of class Column).
max
Normally you'd be correct, but see my updated answer.
dhaffey
Ah very cool!! Thank you. Now if only I could do something about the `__tablename__`, which definitely does have to be unique :) But I like your approach with the mixin class better than my metaclass modification.
max
+1  A: 

I think I got it to work.

I created a metaclass that derives from DeclarativeMeta, and made that the metaclass of C1 and C2. In that new metaclass, I simply said

def __new__(mcs, name, base, attr):
  attr['__tablename__'] = name.lower()
  attr['id'] = Column(Integer, primary_key = True)
  attr['name'] = Column(String)
  return super().__new__(mcs, name, base, attr)

And it seems to work fine.

max