diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2010-10-02 11:00:22 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2010-10-02 11:00:22 -0400 |
commit | 25c08f6def19e1034a887e972ad286ef122473d0 (patch) | |
tree | ba691c6e57197bbb73ee298befa01950219390fa /lib/sqlalchemy/ext/declarative.py | |
parent | 3d389b19b70b65cb76226c3f3aa4c5d926e1f12b (diff) | |
parent | 19d5287d833110507d8ed7b64d31871f67d3f171 (diff) | |
download | sqlalchemy-25c08f6def19e1034a887e972ad286ef122473d0.tar.gz |
merge tip
Diffstat (limited to 'lib/sqlalchemy/ext/declarative.py')
-rwxr-xr-x | lib/sqlalchemy/ext/declarative.py | 243 |
1 files changed, 161 insertions, 82 deletions
diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py index 6d4bdda43..fabd9aaf9 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,13 @@ 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 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 +608,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 +625,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 +664,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 +681,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 +711,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 +738,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 +762,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 +796,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 +811,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__) @@ -865,7 +855,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 +897,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 + ): + 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: @@ -972,7 +976,7 @@ def _as_declarative(cls, classname, dict_): 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 +1087,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 +1116,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, @@ -1192,7 +1214,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." % @@ -1261,6 +1283,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. |