]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- [feature] declared_attr can now be used with
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 27 Aug 2012 20:44:34 +0000 (16:44 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 27 Aug 2012 20:44:34 +0000 (16:44 -0400)
attributes that are not Column or MapperProperty;
including any user-defined value as well
as association proxy objects.  [ticket:2517]

CHANGES
lib/sqlalchemy/ext/declarative/__init__.py
lib/sqlalchemy/ext/declarative/api.py
lib/sqlalchemy/ext/declarative/base.py
test/ext/declarative/test_mixin.py

diff --git a/CHANGES b/CHANGES
index 1f488b013ab351696e2d4a0b50638951658ab4f2..b8eff05552b58d733f5b5ead5ef9a888c27f5a62 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -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
index 8bf03748e21d996bb4490a172bc32883a59cae93..4849a58dcc2744f526ae70eda3f318c3f2b68e1d 100644 (file)
@@ -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
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
index 143468c13645b7d75506669ca3d8f071d8dfa6ef..1a73e4f6d5818e653da5bb1edada012355f651f6 100644 (file)
@@ -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):
index e42ec26452d0b432e2efc3f5da9529f49809d47e..40c8c6ef628eec5dea6e46a1f2d85a9cf7b1b3c9 100644 (file)
@@ -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(
index a77d6be819a1498f9ea222c0393f57d4269a95d5..7bc1e1d1595b9e8ffbe2576474fb4e096f0d162d 100644 (file)
@@ -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):