diff options
| -rw-r--r-- | CHANGES | 12 | ||||
| -rw-r--r-- | lib/sqlalchemy/ext/declarative/__init__.py | 84 | ||||
| -rw-r--r-- | lib/sqlalchemy/ext/declarative/api.py | 11 | ||||
| -rw-r--r-- | lib/sqlalchemy/ext/declarative/base.py | 5 | ||||
| -rw-r--r-- | test/ext/declarative/test_mixin.py | 56 |
5 files changed, 158 insertions, 10 deletions
@@ -242,6 +242,16 @@ underneath "0.7.xx". using a new @declared_attr usage described in the documentation. [ticket:2472] + - [feature] declared_attr can now be used + on non-mixin classes, even though this is generally + only useful for single-inheritance subclass + column conflict resolution. [ticket:2472] + + - [feature] declared_attr can now be used with + attributes that are not Column or MapperProperty; + including any user-defined value as well + as association proxy objects. [ticket:2517] + - [feature] *Very limited* support for inheriting mappers to be GC'ed when the class itself is deferenced. The mapper @@ -287,7 +297,7 @@ underneath "0.7.xx". declared on a single-table inheritance subclass up to the parent class' table, when the parent class is itself mapped to a join() or select() - statement, directly or via joined inheritane, + statement, directly or via joined inheritance, and not just a Table. [ticket:2549] - [bug] An error is emitted when uselist=False diff --git a/lib/sqlalchemy/ext/declarative/__init__.py b/lib/sqlalchemy/ext/declarative/__init__.py index 8bf03748e..4849a58dc 100644 --- a/lib/sqlalchemy/ext/declarative/__init__.py +++ b/lib/sqlalchemy/ext/declarative/__init__.py @@ -909,14 +909,14 @@ to get it's name:: primaryjoin="Target.id==%s.target_id" % cls.__name__ ) -Mixing in deferred(), column_property(), etc. -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Mixing in deferred(), column_property(), and other MapperProperty classes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 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:`.declared_attr` +used with declarative mixins, have the :class:`.declared_attr` requirement so that no reliance on copying is needed:: class SomethingMixin(object): @@ -928,6 +928,84 @@ requirement so that no reliance on copying is needed:: class Something(SomethingMixin, Base): __tablename__ = "something" +Mixing in Association Proxy and Other Attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Mixins can specify user-defined attributes as well as other extension +units such as :func:`.association_proxy`. The usage of :class:`.declared_attr` +is required in those cases where the attribute must be tailored specifically +to the target subclass. An example is when constructing multiple +:func:`.association_proxy` attributes which each target a different type +of child object. Below is an :func:`.association_proxy` / mixin example +which provides a scalar list of string values to an implementing class:: + + from sqlalchemy import Column, Integer, ForeignKey, String + from sqlalchemy.orm import relationship + from sqlalchemy.ext.associationproxy import association_proxy + from sqlalchemy.ext.declarative import declarative_base, declared_attr + + Base = declarative_base() + + class HasStringCollection(object): + @declared_attr + def _strings(cls): + class StringAttribute(Base): + __tablename__ = cls.string_table_name + id = Column(Integer, primary_key=True) + value = Column(String(50), nullable=False) + parent_id = Column(Integer, + ForeignKey('%s.id' % cls.__tablename__), + nullable=False) + def __init__(self, value): + self.value = value + + return relationship(StringAttribute) + + @declared_attr + def strings(cls): + return association_proxy('_strings', 'value') + + class TypeA(HasStringCollection, Base): + __tablename__ = 'type_a' + string_table_name = 'type_a_strings' + id = Column(Integer(), primary_key=True) + + class TypeB(HasStringCollection, Base): + __tablename__ = 'type_b' + string_table_name = 'type_b_strings' + id = Column(Integer(), primary_key=True) + +Above, the ``HasStringCollection`` mixin produces a :func:`.relationship` +which refers to a newly generated class called ``StringAttribute``. The +``StringAttribute`` class is generated with it's own :class:`.Table` +definition which is local to the parent class making usage of the +``HasStringCollection`` mixin. It also produces an :func:`.association_proxy` +object which proxies references to the ``strings`` attribute onto the ``value`` +attribute of each ``StringAttribute`` instance. + +``TypeA`` or ``TypeB`` can be instantiated given the constructor +argument ``strings``, a list of strings:: + + ta = TypeA(strings=['foo', 'bar']) + tb = TypeA(strings=['bat', 'bar']) + +This list will generate a collection +of ``StringAttribute`` objects, which are persisted into a table that's +local to either the ``type_a_strings`` or ``type_b_strings`` table:: + + >>> print ta._strings + [<__main__.StringAttribute object at 0x10151cd90>, + <__main__.StringAttribute object at 0x10151ce10>] + +When constructing the :func:`.association_proxy`, the +:class:`.declared_attr` decorator must be used so that a distinct +:func:`.association_proxy` object is created for each of the ``TypeA`` +and ``TypeB`` classes. + +.. versionadded:: 0.8 :class:`.declared_attr` is usable with non-mapped + attributes, including user-defined attributes as well as + :func:`.association_proxy`. + Controlling table inheritance with mixins ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/lib/sqlalchemy/ext/declarative/api.py b/lib/sqlalchemy/ext/declarative/api.py index 143468c13..1a73e4f6d 100644 --- a/lib/sqlalchemy/ext/declarative/api.py +++ b/lib/sqlalchemy/ext/declarative/api.py @@ -101,11 +101,6 @@ class declared_attr(interfaces._MappedAttribute, property): """Mark a class-level method as representing the definition of a mapped property or special declarative member name. - .. versionchanged:: 0.6.{2,3,4} - ``@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 @@ -146,6 +141,12 @@ class declared_attr(interfaces._MappedAttribute, property): else: return {"polymorphic_identity":cls.__name__} + .. versionchanged:: 0.8 :class:`.declared_attr` can be used with + non-ORM or extension attributes, such as user-defined attributes + or :func:`.association_proxy` objects, which will be assigned + to the class at class construction time. + + """ def __init__(self, fget, *arg, **kw): diff --git a/lib/sqlalchemy/ext/declarative/base.py b/lib/sqlalchemy/ext/declarative/base.py index e42ec2645..40c8c6ef6 100644 --- a/lib/sqlalchemy/ext/declarative/base.py +++ b/lib/sqlalchemy/ext/declarative/base.py @@ -136,7 +136,7 @@ def _as_declarative(cls, classname, dict_): clsregistry.add_class(classname, cls) our_stuff = util.OrderedDict() - for k in dict_: + for k in list(dict_): # TODO: improve this ? all dunders ? if k in ('__table__', '__tablename__', '__mapper_args__'): @@ -153,6 +153,9 @@ def _as_declarative(cls, classname, dict_): "left at the end of the line?" % k) continue if not isinstance(value, (Column, MapperProperty)): + if not k.startswith('__'): + dict_.pop(k) + setattr(cls, k, value) continue if k == 'metadata': raise exc.InvalidRequestError( diff --git a/test/ext/declarative/test_mixin.py b/test/ext/declarative/test_mixin.py index a77d6be81..7bc1e1d15 100644 --- a/test/ext/declarative/test_mixin.py +++ b/test/ext/declarative/test_mixin.py @@ -963,6 +963,62 @@ class DeclarativeMixinTest(DeclarativeTestBase): assert C().x() == 'hi' + def test_arbitrary_attrs_one(self): + class HasMixin(object): + @declared_attr + def some_attr(cls): + return cls.__name__ + "SOME ATTR" + + class Mapped(HasMixin, Base): + __tablename__ = 't' + id = Column(Integer, primary_key=True) + + eq_(Mapped.some_attr, "MappedSOME ATTR") + eq_(Mapped.__dict__['some_attr'], "MappedSOME ATTR") + + def test_arbitrary_attrs_two(self): + from sqlalchemy.ext.associationproxy import association_proxy + + class FilterA(Base): + __tablename__ = 'filter_a' + id = Column(Integer(), primary_key=True) + parent_id = Column(Integer(), + ForeignKey('type_a.id')) + filter = Column(String()) + def __init__(self, filter_, **kw): + self.filter = filter_ + + class FilterB(Base): + __tablename__ = 'filter_b' + id = Column(Integer(), primary_key=True) + parent_id = Column(Integer(), + ForeignKey('type_b.id')) + filter = Column(String()) + def __init__(self, filter_, **kw): + self.filter = filter_ + + class FilterMixin(object): + @declared_attr + def _filters(cls): + return relationship(cls.filter_class, + cascade='all,delete,delete-orphan') + + @declared_attr + def filters(cls): + return association_proxy('_filters', 'filter') + + class TypeA(Base, FilterMixin): + __tablename__ = 'type_a' + filter_class = FilterA + id = Column(Integer(), primary_key=True) + + class TypeB(Base, FilterMixin): + __tablename__ = 'type_b' + filter_class = FilterB + id = Column(Integer(), primary_key=True) + + TypeA(filters=[u'foo']) + TypeB(filters=[u'foo']) class DeclarativeMixinPropertyTest(DeclarativeTestBase): |
