summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES12
-rw-r--r--lib/sqlalchemy/ext/declarative/__init__.py84
-rw-r--r--lib/sqlalchemy/ext/declarative/api.py11
-rw-r--r--lib/sqlalchemy/ext/declarative/base.py5
-rw-r--r--test/ext/declarative/test_mixin.py56
5 files changed, 158 insertions, 10 deletions
diff --git a/CHANGES b/CHANGES
index 1f488b013..b8eff0555 100644
--- 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
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):