summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES6
-rw-r--r--doc/build/orm/extensions/declarative.rst2
-rwxr-xr-xlib/sqlalchemy/ext/declarative.py121
-rw-r--r--lib/sqlalchemy/util.py8
-rw-r--r--test/ext/test_declarative.py107
5 files changed, 174 insertions, 70 deletions
diff --git a/CHANGES b/CHANGES
index 58d95ec54..ed4e5132c 100644
--- a/CHANGES
+++ b/CHANGES
@@ -137,6 +137,12 @@ CHANGES
__mapper_args__, __table_args__, __tablename__ on
a base class that is not a mixin, as well as mixins.
[ticket:1922]
+
+ - @classproperty 's official name/location for usage
+ with declarative is sqlalchemy.ext.declarative.mapperproperty.
+ Same thing, but moving there since it is more of a
+ "marker" that's specific to declararative,
+ not just an attribute technique. [ticket:1915]
- engine
diff --git a/doc/build/orm/extensions/declarative.rst b/doc/build/orm/extensions/declarative.rst
index 010371314..97b94840b 100644
--- a/doc/build/orm/extensions/declarative.rst
+++ b/doc/build/orm/extensions/declarative.rst
@@ -8,6 +8,8 @@ API Reference
.. autofunction:: declarative_base
+.. autoclass:: mapperproperty
+
.. autofunction:: _declarative_constructor
.. autofunction:: has_inherited_table
diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py
index 0b471ee1f..be1cb75ec 100755
--- a/lib/sqlalchemy/ext/declarative.py
+++ b/lib/sqlalchemy/ext/declarative.py
@@ -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:`~.mapperproperty` 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 mapperproperty
class ReferenceAddressMixin(object):
- @classproperty
+ @mapperproperty
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:`~.mapperproperty` 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
+ @mapperproperty
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:`.mapperproperty` 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
+ @mapperproperty
def target_id(cls):
return Column('target_id', ForeignKey('target.id'))
- @classproperty
+ @mapperproperty
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
+ @mapperproperty
def target_id(cls):
return Column('target_id', ForeignKey('target.id'))
- @classproperty
+ @mapperproperty
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:`.mapperproperty`
requirement so that no reliance on copying is needed::
class SomethingMixin(object):
- @classproperty
+ @mapperproperty
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 mapperproperty
class Tablename:
- @classproperty
+ @mapperproperty
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 mapperproperty
from sqlalchemy.ext.declarative import has_inherited_table
class Tablename:
- @classproperty
+ @mapperproperty
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 mapperproperty
from sqlalchemy.ext.declarative import has_inherited_table
class Tablename:
- @classproperty
+ @mapperproperty
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:`.mapperproperty` 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 mapperproperty
class MySQLSettings:
__table_args__ = {'mysql_engine':'InnoDB'}
@@ -821,7 +811,7 @@ from multiple collections::
class MyModel(Base,MySQLSettings,MyOtherMixin):
__tablename__='my_model'
- @classproperty
+ @mapperproperty
def __table_args__(self):
args = dict()
args.update(MySQLSettings.__table_args__)
@@ -907,6 +897,8 @@ def _as_declarative(cls, classname, dict_):
tablename = None
parent_columns = ()
+ declarative_props = (mapperproperty, util.classproperty)
+
for base in cls.__mro__:
class_mapped = _is_mapped_class(base)
if class_mapped:
@@ -916,19 +908,19 @@ def _as_declarative(cls, classname, dict_):
if name == '__mapper_args__':
if not mapper_args and (
not class_mapped or
- isinstance(obj, util.classproperty)
+ isinstance(obj, declarative_props)
):
mapper_args = cls.__mapper_args__
elif name == '__tablename__':
if not tablename and (
not class_mapped or
- isinstance(obj, util.classproperty)
+ isinstance(obj, declarative_props)
):
tablename = cls.__tablename__
elif name == '__table_args__':
if not table_args and (
not class_mapped or
- isinstance(obj, util.classproperty)
+ isinstance(obj, declarative_props)
):
table_args = cls.__table_args__
if base is not cls:
@@ -959,7 +951,7 @@ def _as_declarative(cls, classname, dict_):
"column_property(), relationship(), etc.) must "
"be declared as @classproperty callables "
"on declarative mixin classes.")
- elif isinstance(obj, util.classproperty):
+ elif isinstance(obj, declarative_props):
dict_[name] = ret = \
column_copies[obj] = getattr(cls, name)
if isinstance(ret, (Column, MapperProperty)) and \
@@ -984,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
@@ -1273,6 +1265,63 @@ def comparable_using(comparator_factory):
return comparable_property(comparator_factory, fn)
return decorate
+class mapperproperty(property):
+ """Mark a class-level method as representing the definition of
+ a mapped property or special declarative member name.
+
+ .. note:: @mapperproperty is available as
+ sqlalchemy.util.classproperty for SQLAlchemy versions
+ 0.6.2, 0.6.3, 0.6.4.
+
+ @mapperproperty turns the attribute into a scalar-like
+ property that can be invoked from the uninstantiated class.
+ Declarative treats attributes specifically marked with
+ @mapperproperty 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.
+
+ @mapperproperty 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."
+
+ @mapperproperty
+ 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)
+
+ @mapperproperty
+ def __tablename__(cls):
+ return cls.__name__.lower()
+
+ @mapperproperty
+ 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(mapperproperty, 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.
diff --git a/lib/sqlalchemy/util.py b/lib/sqlalchemy/util.py
index 10931be5e..3b64c5ef1 100644
--- a/lib/sqlalchemy/util.py
+++ b/lib/sqlalchemy/util.py
@@ -1801,8 +1801,12 @@ class classproperty(property):
"""A decorator that behaves like @property except that operates
on classes rather than instances.
- This is helpful when you need to compute __table_args__ and/or
- __mapper_args__ when using declarative."""
+ The decorator is currently special when using the declarative
+ module, but note that the
+ :class:`~.sqlalchemy.ext.declarative.mapperproperty`
+ decorator should be used for this purpose with declarative.
+
+ """
def __init__(self, fget, *arg, **kw):
super(classproperty, self).__init__(fget, *arg, **kw)
diff --git a/test/ext/test_declarative.py b/test/ext/test_declarative.py
index f628d1dc7..c9159f953 100644
--- a/test/ext/test_declarative.py
+++ b/test/ext/test_declarative.py
@@ -14,6 +14,7 @@ from sqlalchemy.orm import relationship, create_session, class_mapper, \
from sqlalchemy.test.testing import eq_
from sqlalchemy.util import classproperty
from test.orm._base import ComparableEntity, MappedTest
+from sqlalchemy.ext.declarative import mapperproperty
class DeclarativeTestBase(testing.TestBase, testing.AssertsExecutionResults):
def setup(self):
@@ -693,7 +694,7 @@ class DeclarativeTest(DeclarativeTestBase):
eq_(sess.query(User).all(), [User(name='u1', address_count=2,
addresses=[Address(email='one'), Address(email='two')])])
- def test_useless_classproperty(self):
+ def test_useless_mapperproperty(self):
class Address(Base, ComparableEntity):
__tablename__ = 'addresses'
@@ -710,7 +711,7 @@ class DeclarativeTest(DeclarativeTestBase):
name = Column('name', String(50))
addresses = relationship('Address', backref='user')
- @classproperty
+ @mapperproperty
def address_count(cls):
# this doesn't really gain us anything. but if
# one is used, lets have it function as expected...
@@ -2197,7 +2198,7 @@ class DeclarativeMixinTest(DeclarativeTestBase):
def test_table_name_inherited(self):
class MyMixin:
- @classproperty
+ @mapperproperty
def __tablename__(cls):
return cls.__name__.lower()
id = Column(Integer, primary_key=True)
@@ -2206,11 +2207,23 @@ class DeclarativeMixinTest(DeclarativeTestBase):
pass
eq_(MyModel.__table__.name, 'mymodel')
+
+ def test_classproperty_still_works(self):
+ class MyMixin(object):
+ @classproperty
+ def __tablename__(cls):
+ return cls.__name__.lower()
+ id = Column(Integer, primary_key=True)
+
+ class MyModel(Base, MyMixin):
+ __tablename__ = 'overridden'
+ eq_(MyModel.__table__.name, 'overridden')
+
def test_table_name_not_inherited(self):
class MyMixin:
- @classproperty
+ @mapperproperty
def __tablename__(cls):
return cls.__name__.lower()
id = Column(Integer, primary_key=True)
@@ -2223,12 +2236,12 @@ class DeclarativeMixinTest(DeclarativeTestBase):
def test_table_name_inheritance_order(self):
class MyMixin1:
- @classproperty
+ @mapperproperty
def __tablename__(cls):
return cls.__name__.lower() + '1'
class MyMixin2:
- @classproperty
+ @mapperproperty
def __tablename__(cls):
return cls.__name__.lower() + '2'
@@ -2240,7 +2253,7 @@ class DeclarativeMixinTest(DeclarativeTestBase):
def test_table_name_dependent_on_subclass(self):
class MyHistoryMixin:
- @classproperty
+ @mapperproperty
def __tablename__(cls):
return cls.parent_name + '_changelog'
@@ -2264,7 +2277,7 @@ class DeclarativeMixinTest(DeclarativeTestBase):
def test_table_args_inherited_descriptor(self):
class MyMixin:
- @classproperty
+ @mapperproperty
def __table_args__(cls):
return {'info': cls.__name__}
@@ -2303,10 +2316,10 @@ class DeclarativeMixinTest(DeclarativeTestBase):
eq_(MyModel.__table__.kwargs, {'mysql_engine': 'InnoDB'})
- def test_mapper_args_classproperty(self):
+ def test_mapper_args_mapperproperty(self):
class ComputedMapperArgs:
- @classproperty
+ @mapperproperty
def __mapper_args__(cls):
if cls.__name__ == 'Person':
return {'polymorphic_on': cls.discriminator}
@@ -2326,13 +2339,13 @@ class DeclarativeMixinTest(DeclarativeTestBase):
is Person.__table__.c.type
eq_(class_mapper(Engineer).polymorphic_identity, 'Engineer')
- def test_mapper_args_classproperty_two(self):
+ def test_mapper_args_mapperproperty_two(self):
- # same as test_mapper_args_classproperty, but we repeat
+ # same as test_mapper_args_mapperproperty, but we repeat
# ComputedMapperArgs on both classes for no apparent reason.
class ComputedMapperArgs:
- @classproperty
+ @mapperproperty
def __mapper_args__(cls):
if cls.__name__ == 'Person':
return {'polymorphic_on': cls.discriminator}
@@ -2367,7 +2380,7 @@ class DeclarativeMixinTest(DeclarativeTestBase):
__tablename__ = 'test'
- @classproperty
+ @mapperproperty
def __table_args__(self):
info = {}
args = dict(info=info)
@@ -2395,7 +2408,7 @@ class DeclarativeMixinTest(DeclarativeTestBase):
class MyMixin:
- @classproperty
+ @mapperproperty
def __mapper_args__(cls):
# tenuous, but illustrates the problem!
@@ -2457,7 +2470,7 @@ class DeclarativeMixinTest(DeclarativeTestBase):
__tablename__ = 'test'
- @classproperty
+ @mapperproperty
def __mapper_args__(cls):
args = {}
args.update(MyMixin1.__mapper_args__)
@@ -2484,15 +2497,15 @@ class DeclarativeMixinTest(DeclarativeTestBase):
def test_mapper_args_property(self):
class MyModel(Base):
- @classproperty
+ @mapperproperty
def __tablename__(cls):
return cls.__name__.lower()
- @classproperty
+ @mapperproperty
def __table_args__(cls):
return {'mysql_engine':'InnoDB'}
- @classproperty
+ @mapperproperty
def __mapper_args__(cls):
args = {}
args['polymorphic_identity'] = cls.__name__
@@ -2513,6 +2526,36 @@ class DeclarativeMixinTest(DeclarativeTestBase):
eq_(MySubModel2.__table__.kwargs['mysql_engine'], 'InnoDB')
eq_(MyModel.__table__.name, 'mymodel')
eq_(MySubModel.__table__.name, 'mysubmodel')
+
+ def test_mapper_args_custom_base(self):
+ """test the @mapperproperty approach from a custom base."""
+
+ class Base(object):
+ @mapperproperty
+ def __tablename__(cls):
+ return cls.__name__.lower()
+
+ @mapperproperty
+ def __table_args__(cls):
+ return {'mysql_engine':'InnoDB'}
+
+ @mapperproperty
+ def id(self):
+ return Column(Integer, primary_key=True)
+
+ Base = decl.declarative_base(cls=Base)
+
+ class MyClass(Base):
+ pass
+
+ class MyOtherClass(Base):
+ pass
+
+ eq_(MyClass.__table__.kwargs['mysql_engine'], 'InnoDB')
+ eq_(MyClass.__table__.name, 'myclass')
+ eq_(MyOtherClass.__table__.name, 'myotherclass')
+ assert MyClass.__table__.c.id.table is MyClass.__table__
+ assert MyOtherClass.__table__.c.id.table is MyOtherClass.__table__
def test_single_table_no_propagation(self):
@@ -2541,7 +2584,7 @@ class DeclarativeMixinTest(DeclarativeTestBase):
class CommonMixin:
- @classproperty
+ @mapperproperty
def __tablename__(cls):
return cls.__name__.lower()
__table_args__ = {'mysql_engine': 'InnoDB'}
@@ -2571,7 +2614,7 @@ class DeclarativeMixinTest(DeclarativeTestBase):
class CommonMixin:
- @classproperty
+ @mapperproperty
def __tablename__(cls):
return cls.__name__.lower()
__table_args__ = {'mysql_engine': 'InnoDB'}
@@ -2608,7 +2651,7 @@ class DeclarativeMixinTest(DeclarativeTestBase):
class NoJoinedTableNameMixin:
- @classproperty
+ @mapperproperty
def __tablename__(cls):
if decl.has_inherited_table(cls):
return None
@@ -2636,7 +2679,7 @@ class DeclarativeMixinTest(DeclarativeTestBase):
class TableNameMixin:
- @classproperty
+ @mapperproperty
def __tablename__(cls):
if decl.has_inherited_table(cls) and TableNameMixin \
not in cls.__bases__:
@@ -2761,7 +2804,7 @@ class DeclarativeMixinPropertyTest(DeclarativeTestBase):
class MyMixin(object):
- @classproperty
+ @mapperproperty
def prop_hoho(cls):
return column_property(Column('prop', String(50)))
@@ -2800,20 +2843,20 @@ class DeclarativeMixinPropertyTest(DeclarativeTestBase):
def test_doc(self):
"""test documentation transfer.
- the documentation situation with @classproperty is problematic.
+ the documentation situation with @mapperproperty is problematic.
at least see if mapped subclasses get the doc.
"""
class MyMixin(object):
- @classproperty
+ @mapperproperty
def type_(cls):
"""this is a document."""
return Column(String(50))
- @classproperty
+ @mapperproperty
def t2(cls):
"""this is another document."""
@@ -2832,7 +2875,7 @@ class DeclarativeMixinPropertyTest(DeclarativeTestBase):
class MyMixin(object):
- @classproperty
+ @mapperproperty
def type_(cls):
return Column(String(50))
__mapper_args__ = {'polymorphic_on': type_}
@@ -2851,7 +2894,7 @@ class DeclarativeMixinPropertyTest(DeclarativeTestBase):
class MyMixin(object):
- @classproperty
+ @mapperproperty
def data(cls):
return deferred(Column('data', String(50)))
@@ -2875,19 +2918,19 @@ class DeclarativeMixinPropertyTest(DeclarativeTestBase):
class RefTargetMixin(object):
- @classproperty
+ @mapperproperty
def target_id(cls):
return Column('target_id', ForeignKey('target.id'))
if usestring:
- @classproperty
+ @mapperproperty
def target(cls):
return relationship('Target',
primaryjoin='Target.id==%s.target_id'
% cls.__name__)
else:
- @classproperty
+ @mapperproperty
def target(cls):
return relationship('Target')