summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/ext/declarative.py
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2010-11-14 17:54:47 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2010-11-14 17:54:47 -0500
commitf252af2b21c5bafeaa30aabcf65dfed9b5c01093 (patch)
treedd5bf4f56ac68d78edfcb37a9c0c3c380c8ef6a8 /lib/sqlalchemy/ext/declarative.py
parent9d7158a2c3869ad7a1ab07d3a41e831f6806a68c (diff)
parent06bf218ed37ca780bc4de2ceb47769c84de70ba1 (diff)
downloadsqlalchemy-f252af2b21c5bafeaa30aabcf65dfed9b5c01093.tar.gz
merge tip
Diffstat (limited to 'lib/sqlalchemy/ext/declarative.py')
-rwxr-xr-xlib/sqlalchemy/ext/declarative.py326
1 files changed, 244 insertions, 82 deletions
diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py
index 10b8fc2d7..3ae81a977 100755
--- a/lib/sqlalchemy/ext/declarative.py
+++ b/lib/sqlalchemy/ext/declarative.py
@@ -358,10 +358,10 @@ and simply pass it to declarative classes::
Base.metadata.reflect(some_engine)
class User(Base):
- __table__ = metadata['user']
+ __table__ = metadata.tables['user']
class Address(Base):
- __table__ = metadata['address']
+ __table__ = metadata.tables['address']
Some configuration schemes may find it more appropriate to use ``__table__``,
such as those which already take advantage of the data-driven nature of
@@ -589,13 +589,14 @@ keys, as a :class:`ForeignKey` itself contains references to columns
which can't be properly recreated at this level. For columns that
have foreign keys, as well as for the variety of mapper-level constructs
that require destination-explicit context, the
-:func:`~sqlalchemy.util.classproperty` decorator is provided so that
+:func:`~.declared_attr` decorator (renamed from ``sqlalchemy.util.classproperty`` in 0.6.5)
+is provided so that
patterns common to many classes can be defined as callables::
- from sqlalchemy.util import classproperty
+ from sqlalchemy.ext.declarative import declared_attr
class ReferenceAddressMixin(object):
- @classproperty
+ @declared_attr
def address_id(cls):
return Column(Integer, ForeignKey('address.id'))
@@ -608,14 +609,14 @@ point at which the ``User`` class is constructed, and the declarative
extension can use the resulting :class:`Column` object as returned by
the method without the need to copy it.
-Columns generated by :func:`~sqlalchemy.util.classproperty` can also be
+Columns generated by :func:`~.declared_attr` can also be
referenced by ``__mapper_args__`` to a limited degree, currently
by ``polymorphic_on`` and ``version_id_col``, by specifying the
classdecorator itself into the dictionary - the declarative extension
will resolve them at class construction time::
class MyMixin:
- @classproperty
+ @declared_attr
def type_(cls):
return Column(String(50))
@@ -625,26 +626,23 @@ will resolve them at class construction time::
__tablename__='test'
id = Column(Integer, primary_key=True)
-.. note:: The usage of :func:`~sqlalchemy.util.classproperty` with mixin
- columns is a new feature as of SQLAlchemy 0.6.2.
-
Mixing in Relationships
~~~~~~~~~~~~~~~~~~~~~~~
Relationships created by :func:`~sqlalchemy.orm.relationship` are provided
with declarative mixin classes exclusively using the
-:func:`~sqlalchemy.util.classproperty` approach, eliminating any ambiguity
+:func:`.declared_attr` approach, eliminating any ambiguity
which could arise when copying a relationship and its possibly column-bound
contents. Below is an example which combines a foreign key column and a
relationship so that two classes ``Foo`` and ``Bar`` can both be configured to
reference a common target class via many-to-one::
class RefTargetMixin(object):
- @classproperty
+ @declared_attr
def target_id(cls):
return Column('target_id', ForeignKey('target.id'))
- @classproperty
+ @declared_attr
def target(cls):
return relationship("Target")
@@ -667,20 +665,16 @@ To reference the mixin class in these expressions, use the given ``cls``
to get it's name::
class RefTargetMixin(object):
- @classproperty
+ @declared_attr
def target_id(cls):
return Column('target_id', ForeignKey('target.id'))
- @classproperty
+ @declared_attr
def target(cls):
return relationship("Target",
primaryjoin="Target.id==%s.target_id" % cls.__name__
)
-.. note:: The usage of :func:`~sqlalchemy.util.classproperty` with mixin
- relationships is a new feature as of SQLAlchemy 0.6.2.
-
-
Mixing in deferred(), column_property(), etc.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -688,21 +682,18 @@ Like :func:`~sqlalchemy.orm.relationship`, all
:class:`~sqlalchemy.orm.interfaces.MapperProperty` subclasses such as
:func:`~sqlalchemy.orm.deferred`, :func:`~sqlalchemy.orm.column_property`,
etc. ultimately involve references to columns, and therefore, when
-used with declarative mixins, have the :func:`~sqlalchemy.util.classproperty`
+used with declarative mixins, have the :func:`.declared_attr`
requirement so that no reliance on copying is needed::
class SomethingMixin(object):
- @classproperty
+ @declared_attr
def dprop(cls):
return deferred(Column(Integer))
class Something(Base, SomethingMixin):
__tablename__ = "something"
-.. note:: The usage of :func:`~sqlalchemy.util.classproperty` with mixin
- mapper properties is a new feature as of SQLAlchemy 0.6.2.
-
Controlling table inheritance with mixins
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -721,10 +712,10 @@ where you wanted to use that mixin in a single table inheritance
hierarchy, you can explicitly specify ``__tablename__`` as ``None`` to
indicate that the class should not have a table mapped::
- from sqlalchemy.util import classproperty
+ from sqlalchemy.ext.declarative import declared_attr
class Tablename:
- @classproperty
+ @declared_attr
def __tablename__(cls):
return cls.__name__.lower()
@@ -748,11 +739,11 @@ has a mapped table.
As an example, here's a mixin that will only allow single table
inheritance::
- from sqlalchemy.util import classproperty
+ from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.declarative import has_inherited_table
class Tablename:
- @classproperty
+ @declared_attr
def __tablename__(cls):
if has_inherited_table(cls):
return None
@@ -772,11 +763,11 @@ table inheritance, you would need a slightly different mixin and use
it on any joined table child classes in addition to their parent
classes::
- from sqlalchemy.util import classproperty
+ from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.declarative import has_inherited_table
class Tablename:
- @classproperty
+ @declared_attr
def __tablename__(cls):
if (has_inherited_table(cls) and
Tablename not in cls.__bases__):
@@ -806,11 +797,11 @@ In the case of ``__table_args__`` or ``__mapper_args__``
specified with declarative mixins, you may want to combine
some parameters from several mixins with those you wish to
define on the class iteself. The
-:func:`~sqlalchemy.util.classproperty` decorator can be used
+:func:`.declared_attr` decorator can be used
here to create user-defined collation routines that pull
from multiple collections::
- from sqlalchemy.util import classproperty
+ from sqlalchemy.ext.declarative import declared_attr
class MySQLSettings:
__table_args__ = {'mysql_engine':'InnoDB'}
@@ -821,7 +812,7 @@ from multiple collections::
class MyModel(Base,MySQLSettings,MyOtherMixin):
__tablename__='my_model'
- @classproperty
+ @declared_attr
def __table_args__(self):
args = dict()
args.update(MySQLSettings.__table_args__)
@@ -830,6 +821,81 @@ from multiple collections::
id = Column(Integer, primary_key=True)
+Defining Indexes in Mixins
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you need to define a multi-column index that applies to all tables
+that make use of a particular mixin, you will need to do this in a
+metaclass as shown in the following example::
+
+ from sqlalchemy.ext.declarative import DeclarativeMeta
+
+ class MyMixinMeta(DeclarativeMeta):
+
+ def __init__(cls,*args,**kw):
+ if getattr(cls,'_decl_class_registry',None) is None:
+ return
+ super(MyMeta,cls).__init__(*args,**kw)
+ # Index creation done here
+ Index('test',cls.a,cls.b)
+
+ class MyMixin(object):
+ __metaclass__=MyMixinMeta
+ a = Column(Integer)
+ b = Column(Integer)
+
+ class MyModel(Base,MyMixin):
+ __tablename__ = 'atable'
+ c = Column(Integer,primary_key=True)
+
+Using multiple Mixins that require Metaclasses
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you end up in a situation where you need to use multiple mixins and
+more than one of them uses a metaclass to, for example, create a
+multi-column index, then you will need to create a metaclass that
+correctly combines the actions of the other metaclasses. For example::
+
+ class MyMeta1(DeclarativeMeta):
+
+ def __init__(cls,*args,**kw):
+ if getattr(cls,'_decl_class_registry',None) is None:
+ return
+ super(MyMeta1,cls).__init__(*args,**kw)
+ Index('ab',cls.a,cls.b)
+
+ class MyMixin1(object):
+ __metaclass__=MyMeta1
+ a = Column(Integer)
+ b = Column(Integer)
+
+ class MyMeta2(DeclarativeMeta):
+
+ def __init__(cls,*args,**kw):
+ if getattr(cls,'_decl_class_registry',None) is None:
+ return
+ super(MyMeta2,cls).__init__(*args,**kw)
+ Index('cd',cls.c,cls.d)
+
+ class MyMixin2(object):
+ __metaclass__=MyMeta2
+ c = Column(Integer)
+ d = Column(Integer)
+
+ class CombinedMeta(MyMeta1,MyMeta2):
+ # This is needed to successfully combine
+ # two mixins which both have metaclasses
+ pass
+
+ class MyModel(Base,MyMixin1,MyMixin2):
+ __tablename__ = 'awooooga'
+ __metaclass__ = CombinedMeta
+ z = Column(Integer,primary_key=True)
+
+For this reason, if a mixin requires a custom metaclass, this should
+be mentioned in any documentation of that mixin to avoid confusion
+later down the line.
+
Class Constructor
=================
@@ -865,7 +931,7 @@ from sqlalchemy.orm.interfaces import MapperProperty
from sqlalchemy.orm.properties import RelationshipProperty, ColumnProperty
from sqlalchemy.orm.util import _is_mapped_class
from sqlalchemy import util, exceptions
-from sqlalchemy.sql import util as sql_util
+from sqlalchemy.sql import util as sql_util, expression
__all__ = 'declarative_base', 'synonym_for', \
@@ -907,56 +973,70 @@ def _as_declarative(cls, classname, dict_):
tablename = None
parent_columns = ()
+ declarative_props = (declared_attr, util.classproperty)
+
for base in cls.__mro__:
- if _is_mapped_class(base):
+ class_mapped = _is_mapped_class(base)
+ if class_mapped:
parent_columns = base.__table__.c.keys()
- else:
- for name,obj in vars(base).items():
- if name == '__mapper_args__':
- if not mapper_args:
- mapper_args = cls.__mapper_args__
- elif name == '__tablename__':
- if not tablename:
- tablename = cls.__tablename__
- elif name == '__table_args__':
- if not table_args:
- table_args = cls.__table_args__
- if base is not cls:
- inherited_table_args = True
- elif base is not cls:
- # we're a mixin.
-
- if isinstance(obj, Column):
- if obj.foreign_keys:
- raise exceptions.InvalidRequestError(
- "Columns with foreign keys to other columns "
- "must be declared as @classproperty callables "
- "on declarative mixin classes. ")
- if name not in dict_ and not (
- '__table__' in dict_ and
- name in dict_['__table__'].c
- ):
- potential_columns[name] = \
- column_copies[obj] = \
- obj.copy()
- column_copies[obj]._creation_order = \
- obj._creation_order
- elif isinstance(obj, MapperProperty):
+
+ for name,obj in vars(base).items():
+ if name == '__mapper_args__':
+ if not mapper_args and (
+ not class_mapped or
+ isinstance(obj, declarative_props)
+ ):
+ mapper_args = cls.__mapper_args__
+ elif name == '__tablename__':
+ if not tablename and (
+ not class_mapped or
+ isinstance(obj, declarative_props)
+ ):
+ tablename = cls.__tablename__
+ elif name == '__table_args__':
+ if not table_args and (
+ not class_mapped or
+ isinstance(obj, declarative_props)
+ ):
+ table_args = cls.__table_args__
+ if base is not cls:
+ inherited_table_args = True
+ elif class_mapped:
+ continue
+ elif base is not cls:
+ # we're a mixin.
+
+ if isinstance(obj, Column):
+ if obj.foreign_keys:
raise exceptions.InvalidRequestError(
- "Mapper properties (i.e. deferred,"
- "column_property(), relationship(), etc.) must "
- "be declared as @classproperty callables "
- "on declarative mixin classes.")
- elif isinstance(obj, util.classproperty):
- dict_[name] = ret = \
- column_copies[obj] = getattr(cls, name)
- if isinstance(ret, (Column, MapperProperty)) and \
- ret.doc is None:
- ret.doc = obj.__doc__
+ "Columns with foreign keys to other columns "
+ "must be declared as @classproperty callables "
+ "on declarative mixin classes. ")
+ if name not in dict_ and not (
+ '__table__' in dict_ and
+ (obj.name or name) in dict_['__table__'].c
+ ) and name not in potential_columns:
+ potential_columns[name] = \
+ column_copies[obj] = \
+ obj.copy()
+ column_copies[obj]._creation_order = \
+ obj._creation_order
+ elif isinstance(obj, MapperProperty):
+ raise exceptions.InvalidRequestError(
+ "Mapper properties (i.e. deferred,"
+ "column_property(), relationship(), etc.) must "
+ "be declared as @classproperty callables "
+ "on declarative mixin classes.")
+ elif isinstance(obj, declarative_props):
+ dict_[name] = ret = \
+ column_copies[obj] = getattr(cls, name)
+ if isinstance(ret, (Column, MapperProperty)) and \
+ ret.doc is None:
+ ret.doc = obj.__doc__
# apply inherited columns as we should
for k, v in potential_columns.items():
- if tablename or k not in parent_columns:
+ if tablename or (v.name or k) not in parent_columns:
dict_[k] = v
if inherited_table_args and not tablename:
@@ -967,12 +1047,19 @@ def _as_declarative(cls, classname, dict_):
for k, v in mapper_args.iteritems():
mapper_args[k] = column_copies.get(v,v)
+
+ if classname in cls._decl_class_registry:
+ util.warn("The classname %r is already in the registry of this"
+ " declarative base, mapped to %r" % (
+ classname,
+ cls._decl_class_registry[classname]
+ ))
cls._decl_class_registry[classname] = cls
our_stuff = util.OrderedDict()
for k in dict_:
value = dict_[k]
- if isinstance(value, util.classproperty):
+ if isinstance(value, declarative_props):
value = getattr(cls, k)
if (isinstance(value, tuple) and len(value) == 1 and
@@ -1083,7 +1170,7 @@ def _as_declarative(cls, classname, dict_):
"Can't place __table_args__ on an inherited class "
"with no table."
)
-
+
# add any columns declared here to the inherited table.
for c in cols:
if c.primary_key:
@@ -1112,7 +1199,25 @@ def _as_declarative(cls, classname, dict_):
set([c.key for c in inherited_table.c
if c not in inherited_mapper._columntoproperty])
exclude_properties.difference_update([c.key for c in cols])
-
+
+ # look through columns in the current mapper that
+ # are keyed to a propname different than the colname
+ # (if names were the same, we'd have popped it out above,
+ # in which case the mapper makes this combination).
+ # See if the superclass has a similar column property.
+ # If so, join them together.
+ for k, col in our_stuff.items():
+ if not isinstance(col, expression.ColumnElement):
+ continue
+ if k in inherited_mapper._props:
+ p = inherited_mapper._props[k]
+ if isinstance(p, ColumnProperty):
+ # note here we place the superclass column
+ # first. this corresponds to the
+ # append() in mapper._configure_property().
+ # change this ordering when we do [ticket:1892]
+ our_stuff[k] = p.columns + [col]
+
cls.__mapper__ = mapper_cls(cls,
table,
properties=our_stuff,
@@ -1193,7 +1298,7 @@ def _deferred_relationship(cls, prop):
return x
except NameError, n:
raise exceptions.InvalidRequestError(
- "When compiling mapper %s, expression %r failed to "
+ "When initializing mapper %s, expression %r failed to "
"locate a name (%r). If this is a class name, consider "
"adding this relationship() to the %r class after "
"both dependent classes have been defined." %
@@ -1262,6 +1367,63 @@ def comparable_using(comparator_factory):
return comparable_property(comparator_factory, fn)
return decorate
+class declared_attr(property):
+ """Mark a class-level method as representing the definition of
+ a mapped property or special declarative member name.
+
+ .. note:: @declared_attr is available as
+ ``sqlalchemy.util.classproperty`` for SQLAlchemy versions
+ 0.6.2, 0.6.3, 0.6.4.
+
+ @declared_attr turns the attribute into a scalar-like
+ property that can be invoked from the uninstantiated class.
+ Declarative treats attributes specifically marked with
+ @declared_attr as returning a construct that is specific
+ to mapping or declarative table configuration. The name
+ of the attribute is that of what the non-dynamic version
+ of the attribute would be.
+
+ @declared_attr is more often than not applicable to mixins,
+ to define relationships that are to be applied to different
+ implementors of the class::
+
+ class ProvidesUser(object):
+ "A mixin that adds a 'user' relationship to classes."
+
+ @declared_attr
+ def user(self):
+ return relationship("User")
+
+ It also can be applied to mapped classes, such as to provide
+ a "polymorphic" scheme for inheritance::
+
+ class Employee(Base):
+ id = Column(Integer, primary_key=True)
+ type = Column(String(50), nullable=False)
+
+ @declared_attr
+ def __tablename__(cls):
+ return cls.__name__.lower()
+
+ @declared_attr
+ def __mapper_args__(cls):
+ if cls.__name__ == 'Employee':
+ return {
+ "polymorphic_on":cls.type,
+ "polymorphic_identity":"Employee"
+ }
+ else:
+ return {"polymorphic_identity":cls.__name__}
+
+ """
+
+ def __init__(self, fget, *arg, **kw):
+ super(declared_attr, self).__init__(fget, *arg, **kw)
+ self.__doc__ = fget.__doc__
+
+ def __get__(desc, self, cls):
+ return desc.fget(cls)
+
def _declarative_constructor(self, **kwargs):
"""A simple constructor that allows initialization from kwargs.