]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- New event hook, MapperEvents.after_configured().
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 10 Sep 2011 20:54:23 +0000 (16:54 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 10 Sep 2011 20:54:23 +0000 (16:54 -0400)
    Called after a configure() step has completed and
    mappers were in fact affected.   Theoretically this
    event is called once per application, unless new mappings
    are constructed after existing ones have been used
    already.

  - New declarative features:
        - __declare_last__() method, establishes an event
        listener for the class method that will be called
        when mappers are completed with the final "configure"
        step.
        - __abstract__ flag.   The class will not be mapped
        at all when this flag is present on the class.
        - New helper classes ConcreteBase, AbstractConcreteBase.
        Allow concrete mappings using declarative which automatically
        set up the "polymorphic_union" when the "configure"
        mapper step is invoked.
        - The mapper itself has semi-private methods that allow
        the "with_polymorphic" selectable to be assigned
        to the mapper after it has already been configured.
        [ticket:2239]

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

diff --git a/CHANGES b/CHANGES
index 72476994b735e7d3ebdd7cbf9369af19f9c7ba36..a98f3d0c462e5cb808f10d984084ee60e2caf750 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -49,6 +49,29 @@ CHANGES
     ArgumentError, rather than UnmappedClassError.
     [ticket:2196]
 
+  - New event hook, MapperEvents.after_configured().  
+    Called after a configure() step has completed and
+    mappers were in fact affected.   Theoretically this
+    event is called once per application, unless new mappings
+    are constructed after existing ones have been used
+    already.
+
+  - New declarative features:
+        - __declare_last__() method, establishes an event
+        listener for the class method that will be called
+        when mappers are completed with the final "configure"
+        step.
+        - __abstract__ flag.   The class will not be mapped
+        at all when this flag is present on the class.
+        - New helper classes ConcreteBase, AbstractConcreteBase.
+        Allow concrete mappings using declarative which automatically
+        set up the "polymorphic_union" when the "configure"
+        mapper step is invoked.
+        - The mapper itself has semi-private methods that allow
+        the "with_polymorphic" selectable to be assigned
+        to the mapper after it has already been configured.
+        [ticket:2239]
+
 -sql
   - Behavioral improvement: empty
     conjunctions such as and_() and or_() will be
index 9e9694df2f6d4004ab536b40a94ee7b681489a85..aaa0261b6dadc3dcb01618450a4fee5da1825e00 100644 (file)
@@ -21,3 +21,8 @@ API Reference
 .. autofunction:: comparable_using
 
 .. autofunction:: instrument_declarative
+
+.. autoclass:: AbstractConcreteBase
+
+.. autoclass:: ConcreteBase
+
index 3b260a7976e3c5a4c442a0f9fa0a2cb0c5c8943d..200b0d6c6e4c5026dd4bab12822e070ebbf9b937 100755 (executable)
@@ -459,10 +459,52 @@ before the class is built::
         __table__ = managers
         __mapper_args__ = {'polymorphic_identity':'manager', 'concrete':True}
 
-There is a recipe which allows the above pattern to be executed
-using the declarative form, via a special base class that defers
-the creation of the mapper.  That recipe is available at
-`DeclarativeAbstractConcreteBase <http://www.sqlalchemy.org/trac/wiki/UsageRecipes/DeclarativeAbstractConcreteBase>`_
+Using the Concrete Helpers
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+New helper classes released in 0.7.3 provides a simpler pattern for concrete inheritance.
+With these objects, the ``__declare_last__`` helper is used to configure the "polymorphic"
+loader for the mapper after all subclasses have been declared.
+
+A basic abstract example of the :class:`.AbstractConcreteBase` class::
+
+    from sqlalchemy.ext.declarative import AbstractConcreteBase
+    
+    class Employee(AbstractConcreteBase, Base):
+        pass
+
+To have a concrete ``employee`` table, use :class:`.ConcreteBase` instead::
+
+    from sqlalchemy.ext.declarative import ConcreteBase
+    
+    class Employee(ConcreteBase, Base):
+        __tablename__ = 'employee'
+        employee_id = Column(Integer, primary_key=True)
+        name = Column(String(50))
+        __mapper_args__ = {
+                        'polymorphic_identity':'employee', 
+                        'concrete':True}
+    
+
+Either ``Employee`` base can be used in the normal fashion::
+
+    class Manager(Employee):
+        __tablename__ = 'manager'
+        employee_id = Column(Integer, primary_key=True)
+        name = Column(String(50))
+        manager_data = Column(String(40))
+        __mapper_args__ = {
+                        'polymorphic_identity':'manager', 
+                        'concrete':True}
+
+    class Engineer(Employee):
+        __tablename__ = 'engineer'
+        employee_id = Column(Integer, primary_key=True)
+        name = Column(String(50))
+        engineer_info = Column(String(40))
+        __mapper_args__ = {'polymorphic_identity':'engineer', 
+                        'concrete':True}
+
 
 .. _declarative_mixins:
 
@@ -825,6 +867,44 @@ it as part of ``__table_args__``::
         __tablename__ = 'atable'
         c =  Column(Integer,primary_key=True)
 
+Special Directives
+==================
+
+``__declare_last__()``
+~~~~~~~~~~~~~~~~~~~~~~
+
+The ``__declare_last__()`` hook, introduced in 0.7.3, allows definition of 
+a class level function that is automatically called by the :meth:`.MapperEvents.after_configured`
+event, which occurs after mappings are assumed to be completed and the 'configure' step
+has finished::
+
+    class MyClass(Base):
+        @classmethod
+        def __declare_last__(cls):
+            ""
+            # do something with mappings
+
+
+``__abstract__``
+~~~~~~~~~~~~~~~~~~~
+
+``__abstract__`` is introduced in 0.7.3 and causes declarative to skip the production
+of a table or mapper for the class entirely.  A class can be added within a hierarchy
+in the same way as mixin (see :ref:`declarative_mixins`), allowing subclasses to extend
+just from the special class::
+
+    class SomeAbstractBase(Base):
+        __abstract__ = True
+        
+        def some_helpful_method(self):
+            ""
+            
+        @declared_attr
+        def __mapper_args__(cls):
+            return {"helpful mapper arguments":True}
+
+    class MyMappedClass(SomeAbstractBase):
+        ""
 
 Class Constructor
 =================
@@ -862,6 +942,8 @@ from sqlalchemy.orm.properties import RelationshipProperty, ColumnProperty, Comp
 from sqlalchemy.orm.util import _is_mapped_class
 from sqlalchemy import util, exc
 from sqlalchemy.sql import util as sql_util, expression
+from sqlalchemy import event
+from sqlalchemy.orm.util import polymorphic_union, _mapper_or_none
 
 
 __all__ = 'declarative_base', 'synonym_for', \
@@ -906,6 +988,18 @@ def _as_declarative(cls, classname, dict_):
     declarative_props = (declared_attr, util.classproperty)
 
     for base in cls.__mro__:
+        _is_declarative_inherits = hasattr(base, '_decl_class_registry')
+
+        if '__declare_last__' in base.__dict__:
+            @event.listens_for(mapper, "after_configured")
+            def go():
+                cls.__declare_last__()
+        if '__abstract__' in base.__dict__:
+            if (base is cls or 
+                (base in cls.__bases__ and not _is_declarative_inherits)
+            ):
+                return
+
         class_mapped = _is_mapped_class(base)
         if class_mapped:
             parent_columns = base.__table__.c.keys()
@@ -1156,8 +1250,8 @@ class DeclarativeMeta(type):
     def __init__(cls, classname, bases, dict_):
         if '_decl_class_registry' in cls.__dict__:
             return type.__init__(cls, classname, bases, dict_)
-
-        _as_declarative(cls, classname, cls.__dict__)
+        else:
+            _as_declarative(cls, classname, cls.__dict__)
         return type.__init__(cls, classname, bases, dict_)
 
     def __setattr__(cls, key, value):
@@ -1454,3 +1548,108 @@ def _undefer_column_name(key, column):
         column.key = key
     if column.name is None:
         column.name = key
+
+class ConcreteBase(object):
+    """A helper class for 'concrete' declarative mappings.
+    
+    :class:`.ConcreteBase` will use the :func:`.polymorphic_union`
+    function automatically, against all tables mapped as a subclass
+    to this class.   The function is called via the
+    ``__declare_last__()`` function, which is essentially
+    a hook for the :func:`.MapperEvents.after_configured` event.
+
+    :class:`.ConcreteBase` produces a mapped
+    table for the class itself.  Compare to :class:`.AbstractConcreteBase`,
+    which does not.
+    
+    Example::
+
+        from sqlalchemy.ext.declarative import ConcreteBase
+
+        class Employee(ConcreteBase, Base):
+            __tablename__ = 'employee'
+            employee_id = Column(Integer, primary_key=True)
+            name = Column(String(50))
+            __mapper_args__ = {
+                            'polymorphic_identity':'employee', 
+                            'concrete':True}
+
+        class Manager(Employee):
+            __tablename__ = 'manager'
+            employee_id = Column(Integer, primary_key=True)
+            name = Column(String(50))
+            manager_data = Column(String(40))
+            __mapper_args__ = {
+                            'polymorphic_identity':'manager', 
+                            'concrete':True}
+
+    """
+
+    @classmethod
+    def _create_polymorphic_union(cls, mappers):
+        return polymorphic_union(dict(
+            (mapper.polymorphic_identity, mapper.local_table)
+            for mapper in mappers
+         ), 'type', 'pjoin')
+
+    @classmethod
+    def __declare_last__(cls):
+        m = cls.__mapper__
+        if m.with_polymorphic:
+            return
+        mappers = [  sm for sm in [
+                    _mapper_or_none(klass)
+                    for klass in cls.__subclasses__()
+                ] if sm is not None] + [m]
+        pjoin = cls._create_polymorphic_union(mappers)
+        m._set_with_polymorphic(("*",pjoin))
+        m._set_polymorphic_on(pjoin.c.type)
+
+class AbstractConcreteBase(ConcreteBase):
+    """A helper class for 'concrete' declarative mappings.
+    
+    :class:`.AbstractConcreteBase` will use the :func:`.polymorphic_union`
+    function automatically, against all tables mapped as a subclass
+    to this class.   The function is called via the
+    ``__declare_last__()`` function, which is essentially
+    a hook for the :func:`.MapperEvents.after_configured` event.
+    
+    :class:`.AbstractConcreteBase` does not produce a mapped
+    table for the class itself.  Compare to :class:`.ConcreteBase`,
+    which does.
+    
+    Example::
+
+        from sqlalchemy.ext.declarative import ConcreteBase
+
+        class Employee(AbstractConcreteBase, Base):
+            pass
+
+        class Manager(Employee):
+            __tablename__ = 'manager'
+            employee_id = Column(Integer, primary_key=True)
+            name = Column(String(50))
+            manager_data = Column(String(40))
+            __mapper_args__ = {
+                            'polymorphic_identity':'manager', 
+                            'concrete':True}
+
+    """
+
+    __abstract__ = True
+
+    @classmethod
+    def __declare_last__(cls):
+        if hasattr(cls, '__mapper__'):
+            return
+        table = cls._create_polymorphic_union(
+            m for m in [
+                _mapper_or_none(klass)
+                for klass in cls.__subclasses__()
+            ] if m is not None
+        )
+        cls.__mapper__ = m = mapper(cls, table, polymorphic_on=table.c.type)
+        for scls in cls.__subclasses__():
+            sm = _mapper_or_none(scls)
+            if sm.concrete and cls in scls.__bases__:
+                sm._set_concrete_base(m)
index 714cc924525bb3d0f12bdd414585aeda27734bb8..d551ecf7a9d21ac5cfdc642c77ff46294742fe78 100644 (file)
@@ -404,6 +404,21 @@ class MapperEvents(event.Events):
         """
         # TODO: need coverage for this event
 
+    def after_configured(self):
+        """Called after a series of mappers have been configured.
+
+        This corresponds to the :func:`.orm.configure_mappers` call, which
+        note is usually called automatically as mappings are first
+        used.
+        
+        Theoretically this event is called once per
+        application, but is actually called any time new mappers
+        have been affected by a :func:`.orm.configure_mappers` call.   If new mappings
+        are constructed after existing ones have already been used, 
+        this event can be called again.
+
+        """
+
     def translate_row(self, mapper, context, row):
         """Perform pre-processing on the given result row and return a
         new row instance.
index 5a2ca6ed45d63a0c06bff2ac0601b82edec48afe..b1a6b1a33f5e13aa360f9e8bd84a4b2d73b73de4 100644 (file)
@@ -151,17 +151,7 @@ class Mapper(object):
 
         self.allow_partial_pks = allow_partial_pks
 
-        if with_polymorphic == '*':
-            self.with_polymorphic = ('*', None)
-        elif isinstance(with_polymorphic, (tuple, list)):
-            if isinstance(with_polymorphic[0], (basestring, tuple, list)):
-                self.with_polymorphic = with_polymorphic
-            else:
-                self.with_polymorphic = (with_polymorphic, None)
-        elif with_polymorphic is not None:
-            raise sa_exc.ArgumentError("Invalid setting for with_polymorphic")
-        else:
-            self.with_polymorphic = None
+        self._set_with_polymorphic(with_polymorphic)
 
         if isinstance(self.local_table, expression._SelectBase):
             raise sa_exc.InvalidRequestError(
@@ -225,10 +215,10 @@ class Mapper(object):
 
     local_table = None
     """The :class:`.Selectable` which this :class:`.Mapper` manages.
-    
+
     Typically is an instance of :class:`.Table` or :class:`.Alias`. 
     May also be ``None``. 
-    
+
     The "local" table is the
     selectable that the :class:`.Mapper` is directly responsible for 
     managing from an attribute access and flush perspective.   For
@@ -239,15 +229,15 @@ class Mapper(object):
     single-table inheriting mapper, local_table will be ``None``.
 
     See also :attr:`~.Mapper.mapped_table`.
-    
+
     """
 
     mapped_table = None
     """The :class:`.Selectable` to which this :class:`.Mapper` is mapped.
-    
+
     Typically an instance of :class:`.Table`, :class:`.Join`, or 
-    :class:`.Alias`.  
-    
+    :class:`.Alias`.
+
     The "mapped" table is the selectable that 
     the mapper selects from during queries.   For non-inheriting 
     mappers, the mapped table is the same as the "local" table.
@@ -255,99 +245,99 @@ class Mapper(object):
     full :class:`.Join` representing full rows for this particular
     subclass.  For single-table inheritance mappers, mapped_table
     references the base table.
-    
+
     See also :attr:`~.Mapper.local_table`.
-    
+
     """
 
     inherits = None
     """References the :class:`.Mapper` which this :class:`.Mapper` 
     inherits from, if any.
 
-    This is a *read only* attribute determined during mapper construction.  
+    This is a *read only* attribute determined during mapper construction.
     Behavior is undefined if directly modified.
-    
+
     """
 
     configured = None
     """Represent ``True`` if this :class:`.Mapper` has been configured.
 
-    This is a *read only* attribute determined during mapper construction.  
+    This is a *read only* attribute determined during mapper construction.
     Behavior is undefined if directly modified.
-    
+
     See also :func:`.configure_mappers`.
-    
+
     """
 
     concrete = None
     """Represent ``True`` if this :class:`.Mapper` is a concrete 
     inheritance mapper.
 
-    This is a *read only* attribute determined during mapper construction.  
+    This is a *read only* attribute determined during mapper construction.
     Behavior is undefined if directly modified.
-    
+
     """
 
     tables = None
     """An iterable containing the collection of :class:`.Table` objects
     which this :class:`.Mapper` is aware of.
-    
+
     If the mapper is mapped to a :class:`.Join`, or an :class:`.Alias`
     representing a :class:`.Select`, the individual :class:`.Table`
     objects that comprise the full construct will be represented here.
 
-    This is a *read only* attribute determined during mapper construction.  
+    This is a *read only* attribute determined during mapper construction.
     Behavior is undefined if directly modified.
-    
+
     """
 
     primary_key = None
     """An iterable containing the collection of :class:`.Column` objects
     which comprise the 'primary key' of the mapped table, from the 
     perspective of this :class:`.Mapper`.
-    
+
     This list is against the selectable in :attr:`~.Mapper.mapped_table`.  In the
     case of inheriting mappers, some columns may be managed by a superclass
     mapper.  For example, in the case of a :class:`.Join`, the primary
     key is determined by all of the primary key columns across all tables
     referenced by the :class:`.Join`.
-    
+
     The list is also not necessarily the same as the primary key column
     collection associated with the underlying tables; the :class:`.Mapper` 
     features a ``primary_key`` argument that can override what the
     :class:`.Mapper` considers as primary key columns.
 
-    This is a *read only* attribute determined during mapper construction.  
+    This is a *read only* attribute determined during mapper construction.
     Behavior is undefined if directly modified.
-    
+
     """
 
     class_ = None
     """The Python class which this :class:`.Mapper` maps.
 
-    This is a *read only* attribute determined during mapper construction.  
+    This is a *read only* attribute determined during mapper construction.
     Behavior is undefined if directly modified.
-    
+
     """
 
     class_manager = None
     """The :class:`.ClassManager` which maintains event listeners
     and class-bound descriptors for this :class:`.Mapper`.
 
-    This is a *read only* attribute determined during mapper construction.  
+    This is a *read only* attribute determined during mapper construction.
     Behavior is undefined if directly modified.
 
     """
 
     single = None
     """Represent ``True`` if this :class:`.Mapper` is a single table 
-    inheritance mapper.   
-    
+    inheritance mapper.
+
     :attr:`~.Mapper.local_table` will be ``None`` if this flag is set.
-    
-    This is a *read only* attribute determined during mapper construction.  
+
+    This is a *read only* attribute determined during mapper construction.
     Behavior is undefined if directly modified.
-    
+
     """
 
     non_primary = None
@@ -355,35 +345,35 @@ class Mapper(object):
     mapper, e.g. a mapper that is used only to selet rows but not for 
     persistence management.
 
-    This is a *read only* attribute determined during mapper construction.  
+    This is a *read only* attribute determined during mapper construction.
     Behavior is undefined if directly modified.
-    
+
     """
 
     polymorphic_on = None
     """The :class:`.Column` specified as the ``polymorphic_on`` column
     for this :class:`.Mapper`, within an inheritance scenario.
-    
+
     This attribute may also be of other types besides :class:`.Column`
     in a future SQLAlchemy release.
 
-    This is a *read only* attribute determined during mapper construction.  
+    This is a *read only* attribute determined during mapper construction.
     Behavior is undefined if directly modified.
-    
+
     """
 
     polymorphic_map = None
     """A mapping of "polymorphic identity" identifiers mapped to :class:`.Mapper`
     instances, within an inheritance scenario.
-    
+
     The identifiers can be of any type which is comparable to the 
     type of column represented by :attr:`~.Mapper.polymorphic_on`.
-    
+
     An inheritance chain of mappers will all reference the same 
     polymorphic map object.  The object is used to correlate incoming
     result rows to target mappers.
 
-    This is a *read only* attribute determined during mapper construction.  
+    This is a *read only* attribute determined during mapper construction.
     Behavior is undefined if directly modified.
 
     """
@@ -391,32 +381,32 @@ class Mapper(object):
     polymorphic_identity = None
     """Represent an identifier which is matched against the :attr:`~.Mapper.polymorphic_on`
     column during result row loading.
-    
+
     Used only with inheritance, this object can be of any type which is
     comparable to the type of column represented by :attr:`~.Mapper.polymorphic_on`.
-    
-    This is a *read only* attribute determined during mapper construction.  
+
+    This is a *read only* attribute determined during mapper construction.
     Behavior is undefined if directly modified.
-    
+
     """
 
     base_mapper = None
     """The base-most :class:`.Mapper` in an inheritance chain.
-    
+
     In a non-inheriting scenario, this attribute will always be this
     :class:`.Mapper`.   In an inheritance scenario, it references
     the :class:`.Mapper` which is parent to all other :class:`.Mapper`
     objects in the inheritance chain.
 
-    This is a *read only* attribute determined during mapper construction.  
+    This is a *read only* attribute determined during mapper construction.
     Behavior is undefined if directly modified.
-    
+
     """
 
     columns = None
     """A collection of :class:`.Column` or other scalar expression 
     objects maintained by this :class:`.Mapper`.
-    
+
     The collection behaves the same as that of the ``c`` attribute on 
     any :class:`.Table` object, except that only those columns included in
     this mapping are present, and are keyed based on the attribute name
@@ -424,9 +414,9 @@ class Mapper(object):
     :class:`.Column` itself.   Additionally, scalar expressions mapped
     by :func:`.column_property` are also present here.
 
-    This is a *read only* attribute determined during mapper construction.  
+    This is a *read only* attribute determined during mapper construction.
     Behavior is undefined if directly modified.
-    
+
     """
 
     validators = None
@@ -530,16 +520,6 @@ class Mapper(object):
             if self.polymorphic_identity is not None:
                 self.polymorphic_map[self.polymorphic_identity] = self
 
-            if self.polymorphic_on is None:
-                for mapper in self.iterate_to_root():
-                    # try to set up polymorphic on using
-                    # correesponding_column(); else leave
-                    # as None
-                    if mapper.polymorphic_on is not None:
-                        self.polymorphic_on = \
-                                self.mapped_table.corresponding_column(
-                                                    mapper.polymorphic_on)
-                        break
         else:
             self._all_tables = set()
             self.base_mapper = self
@@ -553,6 +533,59 @@ class Mapper(object):
                     "Mapper '%s' does not have a mapped_table specified." 
                     % self)
 
+    def _set_with_polymorphic(self, with_polymorphic):
+        if with_polymorphic == '*':
+            self.with_polymorphic = ('*', None)
+        elif isinstance(with_polymorphic, (tuple, list)):
+            if isinstance(with_polymorphic[0], (basestring, tuple, list)):
+                self.with_polymorphic = with_polymorphic
+            else:
+                self.with_polymorphic = (with_polymorphic, None)
+        elif with_polymorphic is not None:
+            raise sa_exc.ArgumentError("Invalid setting for with_polymorphic")
+        else:
+            self.with_polymorphic = None
+
+        if isinstance(self.local_table, expression._SelectBase):
+            raise sa_exc.InvalidRequestError(
+                "When mapping against a select() construct, map against "
+                "an alias() of the construct instead."
+                "This because several databases don't allow a "
+                "SELECT from a subquery that does not have an alias."
+                )
+
+        if self.with_polymorphic and \
+                    isinstance(self.with_polymorphic[1],
+                                expression._SelectBase):
+            self.with_polymorphic = (self.with_polymorphic[0],
+                                self.with_polymorphic[1].alias())
+        if self.configured:
+            self._expire_memoizations()
+
+    def _set_concrete_base(self, mapper):
+        """Set the given :class:`.Mapper` as the 'inherits' for this :class:`.Mapper`,
+        assuming this :class:`.Mapper` is concrete and does not already have
+        an inherits."""
+
+        assert self.concrete
+        assert not self.inherits
+        assert isinstance(mapper, Mapper)
+        self.inherits = mapper
+        self.inherits.polymorphic_map.update(self.polymorphic_map)
+        self.polymorphic_map = self.inherits.polymorphic_map
+        for mapper in self.iterate_to_root():
+            if mapper.polymorphic_on is not None:
+                mapper._requires_row_aliasing = True
+        self.batch = self.inherits.batch
+        self.base_mapper = self.inherits.base_mapper
+        self.inherits._inheriting_mappers.add(self)
+        self.passive_updates = self.inherits.passive_updates
+        self._all_tables = self.inherits._all_tables
+
+    def _set_polymorphic_on(self, polymorphic_on):
+        self.polymorphic_on = polymorphic_on
+        self._configure_polymorphic_setter(True)
+
     def _configure_legacy_instrument_class(self):
 
         if self.inherits:
@@ -818,7 +851,7 @@ class Mapper(object):
                                     init=False, 
                                     setparent=True)
 
-    def _configure_polymorphic_setter(self):
+    def _configure_polymorphic_setter(self, init=False):
         """Configure an attribute on the mapper representing the 
         'polymorphic_on' column, if applicable, and not 
         already generated by _configure_properties (which is typical).
@@ -834,6 +867,17 @@ class Mapper(object):
         # but we need it for the base mapper
         setter = False
 
+        if self.polymorphic_on is None:
+            for mapper in self.iterate_to_root():
+                # try to set up polymorphic on using
+                # correesponding_column(); else leave
+                # as None
+                if mapper.polymorphic_on is not None:
+                    self.polymorphic_on = \
+                            self.mapped_table.corresponding_column(
+                                                mapper.polymorphic_on)
+                    break
+
         if self.polymorphic_on is not None:
             setter = True
 
@@ -861,7 +905,7 @@ class Mapper(object):
                 self._configure_property(
                                 col.key, 
                                 properties.ColumnProperty(col, _instrument=instrument),
-                                init=False, setparent=True)
+                                init=init, setparent=True)
                 polymorphic_key = col.key
             else:
                 polymorphic_key = self._columntoproperty[self.polymorphic_on].key
@@ -2714,6 +2758,7 @@ def configure_mappers():
     if not _new_mappers:
         return 
 
+    _call_configured = None
     _COMPILE_MUTEX.acquire()
     try:
         global _already_compiling
@@ -2744,6 +2789,7 @@ def configure_mappers():
                         mapper._post_configure_properties()
                         mapper._expire_memoizations()
                         mapper.dispatch.mapper_configured(mapper, mapper.class_)
+                        _call_configured = mapper
                     except:
                         exc = sys.exc_info()[1]
                         if not hasattr(exc, '_configure_failed'):
@@ -2755,7 +2801,8 @@ def configure_mappers():
             _already_compiling = False
     finally:
         _COMPILE_MUTEX.release()
-
+    if _call_configured is not None:
+        _call_configured.dispatch.after_configured()
 
 def reconstructor(fn):
     """Decorate a method as the 'reconstructor' hook.
index 3dc1f86763b139207c864c4b5e4788bbda944cf1..d778bef3d70315c421611e2f98998d090b34ac8b 100644 (file)
@@ -601,6 +601,9 @@ def has_identity(object):
     return state.has_identity
 
 def _is_mapped_class(cls):
+    """Return True if the given object is a mapped class, 
+    :class:`.Mapper`, or :class:`.AliasedClass`."""
+
     if isinstance(cls, (AliasedClass, mapperlib.Mapper)):
         return True
     if isinstance(cls, expression.ClauseElement):
@@ -610,6 +613,16 @@ def _is_mapped_class(cls):
         return manager and _INSTRUMENTOR in manager.info
     return False
 
+def _mapper_or_none(cls):
+    """Return the :class:`.Mapper` for the given class or None if the 
+    class is not mapped."""
+
+    manager = attributes.manager_of_class(cls)
+    if manager is not None and _INSTRUMENTOR in manager.info:
+        return manager.info[_INSTRUMENTOR]
+    else:
+        return None
+
 def instance_str(instance):
     """Return a string describing an instance."""
 
index 22733223ded516e2d92f696951525ff7b4e44592..e338ef0396b34ec42061cf2ca673e992bdea38cb 100644 (file)
@@ -14,7 +14,7 @@ from sqlalchemy.orm import relationship, create_session, class_mapper, \
     Session
 from test.lib.testing import eq_
 from sqlalchemy.util import classproperty
-from sqlalchemy.ext.declarative import declared_attr
+from sqlalchemy.ext.declarative import declared_attr, AbstractConcreteBase, ConcreteBase
 from test.lib import fixtures
 
 class DeclarativeTestBase(fixtures.TestBase, testing.AssertsExecutionResults):
@@ -2014,7 +2014,30 @@ class DeclarativeInheritanceTest(DeclarativeTestBase):
 
         assert A.__mapper__.inherits is a_1.__mapper__
 
-    def test_concrete(self):
+from test.orm.test_events import _RemoveListeners
+class ConcreteInhTest(_RemoveListeners, DeclarativeTestBase):
+    def _roundtrip(self, Employee, Manager, Engineer, polymorphic=True):
+        Base.metadata.create_all()
+        sess = create_session()
+        e1 = Engineer(name='dilbert', primary_language='java')
+        e2 = Engineer(name='wally', primary_language='c++')
+        m1 = Manager(name='dogbert', golf_swing='fore!')
+        e3 = Engineer(name='vlad', primary_language='cobol')
+        sess.add_all([e1, e2, m1, e3])
+        sess.flush()
+        sess.expunge_all()
+        if polymorphic:
+            eq_(sess.query(Employee).order_by(Employee.name).all(),
+                [Engineer(name='dilbert'), Manager(name='dogbert'),
+                Engineer(name='vlad'), Engineer(name='wally')])
+        else:
+            eq_(sess.query(Engineer).order_by(Engineer.name).all(),
+                [Engineer(name='dilbert'), Engineer(name='vlad'),
+                Engineer(name='wally')])
+            eq_(sess.query(Manager).all(), [Manager(name='dogbert')])
+
+
+    def test_explicit(self):
         engineers = Table('engineers', Base.metadata, Column('id',
                           Integer, primary_key=True,
                           test_needs_autoincrement=True), Column('name'
@@ -2027,47 +2050,35 @@ class DeclarativeInheritanceTest(DeclarativeTestBase):
         punion = polymorphic_union({'engineer': engineers, 'manager'
                                    : managers}, 'type', 'punion')
 
-        class Person(Base, fixtures.ComparableEntity):
+        class Employee(Base, fixtures.ComparableEntity):
 
             __table__ = punion
             __mapper_args__ = {'polymorphic_on': punion.c.type}
 
-        class Engineer(Person):
+        class Engineer(Employee):
 
             __table__ = engineers
             __mapper_args__ = {'polymorphic_identity': 'engineer',
                                'concrete': True}
 
-        class Manager(Person):
+        class Manager(Employee):
 
             __table__ = managers
             __mapper_args__ = {'polymorphic_identity': 'manager',
                                'concrete': True}
-
-        Base.metadata.create_all()
-        sess = create_session()
-        e1 = Engineer(name='dilbert', primary_language='java')
-        e2 = Engineer(name='wally', primary_language='c++')
-        m1 = Manager(name='dogbert', golf_swing='fore!')
-        e3 = Engineer(name='vlad', primary_language='cobol')
-        sess.add_all([e1, e2, m1, e3])
-        sess.flush()
-        sess.expunge_all()
-        eq_(sess.query(Person).order_by(Person.name).all(),
-            [Engineer(name='dilbert'), Manager(name='dogbert'),
-            Engineer(name='vlad'), Engineer(name='wally')])
+        self._roundtrip(Employee, Manager, Engineer)
 
     def test_concrete_inline_non_polymorphic(self):
         """test the example from the declarative docs."""
 
-        class Person(Base, fixtures.ComparableEntity):
+        class Employee(Base, fixtures.ComparableEntity):
 
             __tablename__ = 'people'
             id = Column(Integer, primary_key=True,
                         test_needs_autoincrement=True)
             name = Column(String(50))
 
-        class Engineer(Person):
+        class Engineer(Employee):
 
             __tablename__ = 'engineers'
             __mapper_args__ = {'concrete': True}
@@ -2076,7 +2087,7 @@ class DeclarativeInheritanceTest(DeclarativeTestBase):
             primary_language = Column(String(50))
             name = Column(String(50))
 
-        class Manager(Person):
+        class Manager(Employee):
 
             __tablename__ = 'manager'
             __mapper_args__ = {'concrete': True}
@@ -2084,20 +2095,57 @@ class DeclarativeInheritanceTest(DeclarativeTestBase):
                         test_needs_autoincrement=True)
             golf_swing = Column(String(50))
             name = Column(String(50))
+        self._roundtrip(Employee, Manager, Engineer, polymorphic=False)
+
+    def test_abstract_concrete_extension(self):
+        class Employee(AbstractConcreteBase, Base, fixtures.ComparableEntity):
+            pass
+
+        class Manager(Employee):
+            __tablename__ = 'manager'
+            employee_id = Column(Integer, primary_key=True)
+            name = Column(String(50))
+            golf_swing = Column(String(40))
+            __mapper_args__ = {
+                            'polymorphic_identity':'manager', 
+                            'concrete':True}
+
+        class Engineer(Employee):
+            __tablename__ = 'engineer'
+            employee_id = Column(Integer, primary_key=True)
+            name = Column(String(50))
+            primary_language = Column(String(40))
+            __mapper_args__ = {'polymorphic_identity':'engineer', 
+                            'concrete':True}
+
+        self._roundtrip(Employee, Manager, Engineer)
+
+    def test_concrete_extension(self):
+        class Employee(ConcreteBase, Base, fixtures.ComparableEntity):
+            __tablename__ = 'employee'
+            employee_id = Column(Integer, primary_key=True)
+            name = Column(String(50))
+            __mapper_args__ = {
+                            'polymorphic_identity':'employee', 
+                            'concrete':True}
+        class Manager(Employee):
+            __tablename__ = 'manager'
+            employee_id = Column(Integer, primary_key=True)
+            name = Column(String(50))
+            golf_swing = Column(String(40))
+            __mapper_args__ = {
+                            'polymorphic_identity':'manager', 
+                            'concrete':True}
+
+        class Engineer(Employee):
+            __tablename__ = 'engineer'
+            employee_id = Column(Integer, primary_key=True)
+            name = Column(String(50))
+            primary_language = Column(String(40))
+            __mapper_args__ = {'polymorphic_identity':'engineer', 
+                            'concrete':True}
+        self._roundtrip(Employee, Manager, Engineer)
 
-        Base.metadata.create_all()
-        sess = create_session()
-        e1 = Engineer(name='dilbert', primary_language='java')
-        e2 = Engineer(name='wally', primary_language='c++')
-        m1 = Manager(name='dogbert', golf_swing='fore!')
-        e3 = Engineer(name='vlad', primary_language='cobol')
-        sess.add_all([e1, e2, m1, e3])
-        sess.flush()
-        sess.expunge_all()
-        eq_(sess.query(Engineer).order_by(Engineer.name).all(),
-            [Engineer(name='dilbert'), Engineer(name='vlad'),
-            Engineer(name='wally')])
-        eq_(sess.query(Manager).all(), [Manager(name='dogbert')])
 
 def _produce_test(inline, stringbased):