]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- @classproperty 's official name/location for usage
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 25 Sep 2010 23:25:31 +0000 (19:25 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 25 Sep 2010 23:25:31 +0000 (19:25 -0400)
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]

CHANGES
doc/build/orm/extensions/declarative.rst
lib/sqlalchemy/ext/declarative.py
lib/sqlalchemy/util.py
test/ext/test_declarative.py

diff --git a/CHANGES b/CHANGES
index 58d95ec5416cf69512f70a9f883754a9856a65a6..ed4e5132cc80f0162ef134e64675caaceb48ed16 100644 (file)
--- 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
    
index 010371314c8a4daec82cd259b68597eb24ded04d..97b94840b7d2425f0410c076f6c067248b470a48 100644 (file)
@@ -8,6 +8,8 @@ API Reference
 
 .. autofunction:: declarative_base
 
+.. autoclass:: mapperproperty
+
 .. autofunction:: _declarative_constructor
 
 .. autofunction:: has_inherited_table
index 0b471ee1fbc1fca787947343bb38b718498a4d03..be1cb75ec94766852f014baf4838887f24ba1abe 100755 (executable)
@@ -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.
 
index 10931be5e5506081f60da92a485af79c3e83baab..3b64c5ef1f611a72b963fedc4d451453f78f6ae5 100644 (file)
@@ -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)
index f628d1dc74ec7dd118a32affb66f01d7773d28af..c9159f9537336069f8946f47e0c7299108c394d5 100644 (file)
@@ -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')