]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- base_columns on ColumnElement becomes a list; as usual, because columns in Compound...
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 5 Nov 2007 17:18:21 +0000 (17:18 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 5 Nov 2007 17:18:21 +0000 (17:18 +0000)
may extend from more than one root column.
- keys_ok argument from corresponding_column() removed.  no more name-based matching of columns anywhere.
- DictDecorator is gone.  all row translators provided by orm.util.create_row_adapter().  Mapper
and contains_alias() cache the adapters on target mapper to avoid re-computation of adapters.
- create_row_adapter() accepts an "equivalent_columns" map as produced by Mapper, so that
row adapters can take join conditions into account (as usual again, to help with the CompoundSelects
produced by polymorphic_union).
- simplified TableSingleton to just provide lookup; moved all initialization into Table.
- the "properties" accessor on Mapper is removed; it now throws an informative
exception explaining the usage of mapper.get_property() and
mapper.iterate_properties

CHANGES
lib/sqlalchemy/orm/__init__.py
lib/sqlalchemy/orm/mapper.py
lib/sqlalchemy/orm/util.py
lib/sqlalchemy/schema.py
lib/sqlalchemy/sql/expression.py
lib/sqlalchemy/sql/util.py
lib/sqlalchemy/util.py
test/orm/mapper.py

diff --git a/CHANGES b/CHANGES
index 95ff2a4108b8cfb9fcd1099a9c1ce4b0fbd967c3..7c0c77296e176f3ef6df06ea35be66f8acb362f2 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -48,6 +48,10 @@ CHANGES
     with arbitrary sementics.  The orm now handles all mapped instances on
     an identity-only basis. (e.g. 'is' vs '==') [ticket:676]
 
+  - the "properties" accessor on Mapper is removed; it now throws an informative
+    exception explaining the usage of mapper.get_property() and 
+    mapper.iterate_properties
+    
   - The behavior of query.options() is now fully based on paths, i.e. an
     option such as eagerload_all('x.y.z.y.x') will apply eagerloading to
     only those paths, i.e. and not 'x.y.x'; eagerload('children.children')
index 553bd57baf51e96f341461198a8e5c49d7139bde..90a172e624780755548a2910a8c918ca46c6858a 100644 (file)
@@ -17,7 +17,7 @@ from sqlalchemy.orm.properties import PropertyLoader, ColumnProperty, CompositeP
 from sqlalchemy.orm import mapper as mapperlib
 from sqlalchemy.orm import strategies
 from sqlalchemy.orm.query import Query
-from sqlalchemy.orm.util import polymorphic_union
+from sqlalchemy.orm.util import polymorphic_union, create_row_adapter
 from sqlalchemy.orm.session import Session as _Session
 from sqlalchemy.orm.session import object_session, attribute_manager, sessionmaker
 from sqlalchemy.orm.scoping import ScopedSession
@@ -621,18 +621,18 @@ def contains_alias(alias):
                 self.selectable = None
             else:
                 self.selectable = alias
+            self._row_translators = {}
         def get_selectable(self, mapper):
             if self.selectable is None:
                 self.selectable = mapper.mapped_table.alias(self.alias)
             return self.selectable
         def translate_row(self, mapper, context, row):
-            newrow = sautil.DictDecorator(row)
-            selectable = self.get_selectable(mapper)
-            for c in mapper.mapped_table.c:
-                c2 = selectable.corresponding_column(c, keys_ok=True, raiseerr=False)
-                if c2 and c2 in row:
-                    newrow[c] = row[c2]
-            return newrow
+            if mapper in self._row_translators:
+                return self._row_translators[mapper](row)
+            else:
+                translator = create_row_adapter(self.get_selectable(mapper), mapper.mapped_table)
+                self._row_translators[mapper] = translator
+                return translator(row)
 
     return ExtensionOption(AliasedRow(alias))
 
index efc509725220f9b3ca96d07c7d410e117fc8ffa8..3cfe7b3f61a6615810a650164ebdd41c22676993 100644 (file)
@@ -9,7 +9,7 @@ from sqlalchemy import sql, util, exceptions, logging
 from sqlalchemy.sql import expression
 from sqlalchemy.sql import util as sqlutil
 from sqlalchemy.orm import util as mapperutil
-from sqlalchemy.orm.util import ExtensionCarrier
+from sqlalchemy.orm.util import ExtensionCarrier, create_row_adapter
 from sqlalchemy.orm import sync
 from sqlalchemy.orm.interfaces import MapperProperty, EXT_CONTINUE, SynonymProperty, PropComparator
 deferred_load = None
@@ -102,7 +102,7 @@ class Mapper(object):
         self.inherit_condition = inherit_condition
         self.inherit_foreign_keys = inherit_foreign_keys
         self.extension = extension
-        self.properties = properties or {}
+        self._init_properties = properties or {}
         self.allow_column_override = allow_column_override
         self.allow_null_pks = allow_null_pks
         self.delete_orphans = []
@@ -110,7 +110,8 @@ class Mapper(object):
         self.column_prefix = column_prefix
         self.polymorphic_on = polymorphic_on
         self._eager_loaders = util.Set()
-
+        self._row_translators = {}
+        
         # our 'polymorphic identity', a string name that when located in a result set row
         # indicates this Mapper should be used to construct the object instance for that row.
         self.polymorphic_identity = polymorphic_identity
@@ -195,6 +196,10 @@ class Mapper(object):
         return self.__props.itervalues()
     iterate_properties = property(iterate_properties, doc="returns an iterator of all MapperProperty objects.")
     
+    def properties(self):
+        raise NotImplementedError("Public collection of MapperProperty objects is provided by the get_property() and iterate_properties accessors.")
+    properties = property(properties)
+    
     def dispose(self):
         # disaable any attribute-based compilation
         self.__props_init = True
@@ -324,7 +329,7 @@ class Mapper(object):
                 self.inherits._add_polymorphic_mapping(self.polymorphic_identity, self)
                 if self.polymorphic_on is None:
                     if self.inherits.polymorphic_on is not None:
-                        self.polymorphic_on = self.mapped_table.corresponding_column(self.inherits.polymorphic_on, keys_ok=True, raiseerr=False)
+                        self.polymorphic_on = self.mapped_table.corresponding_column(self.inherits.polymorphic_on, raiseerr=False)
                     else:
                         raise exceptions.ArgumentError("Mapper '%s' specifies a polymorphic_identity of '%s', but no mapper in it's hierarchy specifies the 'polymorphic_on' column argument" % (str(self), self.polymorphic_identity))
 
@@ -416,19 +421,20 @@ class Mapper(object):
         if self.inherits is not None and not self.concrete and not self.primary_key_argument:
             self.primary_key = self.inherits.primary_key
             self._get_clause = self.inherits._get_clause
+            self._equivalent_columns = {}
         else:
             # create the "primary_key" for this mapper.  this will flatten "equivalent" primary key columns
             # into one column, where "equivalent" means that one column references the other via foreign key, or
             # multiple columns that all reference a common parent column.  it will also resolve the column
             # against the "mapped_table" of this mapper.
-            equivalent_columns = self._get_equivalent_columns()
+            self._equivalent_columns = self._get_equivalent_columns()
         
             primary_key = expression.ColumnSet()
 
             for col in (self.primary_key_argument or self.pks_by_table[self.mapped_table]):
                 c = self.mapped_table.corresponding_column(col, raiseerr=False)
                 if c is None:
-                    for cc in equivalent_columns[col]:
+                    for cc in self._equivalent_columns[col]:
                         c = self.mapped_table.corresponding_column(cc, raiseerr=False)
                         if c is not None:
                             break
@@ -494,9 +500,6 @@ class Mapper(object):
                 set([tabled.col2])
         }
         
-        this method is called repeatedly during the compilation process as 
-        the resulting dictionary contains more equivalents as more inheriting 
-        mappers are compiled.  the repetition process may be open to some optimization.
         """
 
         result = {}
@@ -565,8 +568,8 @@ class Mapper(object):
         self._columntoproperty = mapperutil.TranslatingDict(self.mapped_table)
 
         # load custom properties
-        if self.properties is not None:
-            for key, prop in self.properties.iteritems():
+        if self._init_properties is not None:
+            for key, prop in self._init_properties.iteritems():
                 self._compile_property(key, prop, False)
 
         # pull properties from the inherited mapper if any.
@@ -641,7 +644,7 @@ class Mapper(object):
 
         if isinstance(prop, ColumnProperty):
             # relate the mapper's "select table" to the given ColumnProperty
-            col = self.select_table.corresponding_column(prop.columns[0], keys_ok=True, raiseerr=False)
+            col = self.select_table.corresponding_column(prop.columns[0], raiseerr=False)
             # col might not be present! the selectable given to the mapper need not include "deferred"
             # columns (included in zblog tests)
             if col is None:
@@ -680,8 +683,8 @@ class Mapper(object):
 
         if self.select_table is not self.mapped_table:
             props = {}
-            if self.properties is not None:
-                for key, prop in self.properties.iteritems():
+            if self._init_properties is not None:
+                for key, prop in self._init_properties.iteritems():
                     if expression.is_column(prop):
                         props[key] = self.select_table.corresponding_column(prop)
                     elif (isinstance(prop, list) and expression.is_column(prop[0])):
@@ -779,7 +782,7 @@ class Mapper(object):
         the given MapperProperty is compiled immediately.
         """
 
-        self.properties[key] = prop
+        self._init_properties[key] = prop
         self._compile_property(key, prop, init=self.__props_init)
 
     def __str__(self):
@@ -1469,13 +1472,13 @@ class Mapper(object):
         This can be used in conjunction with populate_instance to
         populate an instance using an alternate mapper.
         """
-
-        newrow = util.DictDecorator(row)
-        for c in tomapper.mapped_table.c:
-            c2 = self.mapped_table.corresponding_column(c, keys_ok=True, raiseerr=False)
-            if c2 and c2 in row:
-                newrow[c] = row[c2]
-        return newrow
+        
+        if tomapper in self._row_translators:
+            return self._row_translators[tomapper](row)
+        else:
+            translator = create_row_adapter(self.mapped_table, tomapper.mapped_table, equivalent_columns=self._equivalent_columns)
+            self._row_translators[tomapper] = translator
+            return translator(row)
 
     def populate_instance(self, selectcontext, instance, row, ispostselect=None, isnew=False, **flags):
         """populate an instance from a result row."""
index 87b74c3c27b13b38cdf369c8611d186a54253167..de9694bb2feed72e9fd3b7dbea3a61e0d280382d 100644 (file)
@@ -91,7 +91,7 @@ class TranslatingDict(dict):
         self.selectable = selectable
 
     def __translate_col(self, col):
-        ourcol = self.selectable.corresponding_column(col, keys_ok=False, raiseerr=False)
+        ourcol = self.selectable.corresponding_column(col, raiseerr=False)
         if ourcol is None:
             return col
         else:
@@ -226,10 +226,27 @@ class AliasedClauses(object):
         """
         return create_row_adapter(self.alias, self.mapped_table)
 
-def create_row_adapter(from_, to):
-    map = {}        
+def create_row_adapter(from_, to, equivalent_columns=None):
+    """create a row adapter between two selectables.
+    
+    The returned adapter is a class that can be instantiated repeatedly for any number
+    of rows; this is an inexpensive process.  However, the creation of the row
+    adapter class itself *is* fairly expensive so caching should be used to prevent
+    repeated calls to this function.
+    """
+    
+    map = {}
     for c in to.c:
-        map[c] = from_.corresponding_column(c)
+        corr = from_.corresponding_column(c, raiseerr=False)
+        if corr:
+            map[c] = corr
+        elif equivalent_columns:
+            if c in equivalent_columns:
+                for c2 in equivalent_columns[c]:
+                    corr = from_.corresponding_column(c2, raiseerr=False)
+                    if corr:
+                        map[c] = corr
+                        break
 
     class AliasedRow(object):
         def __init__(self, row):
index 5ddca718a86ce774233659a85be82ead879a5230..a7f24a211a19bafb66c33e4bbe8a5e9fa690f676 100644 (file)
@@ -86,11 +86,8 @@ class _TableSingleton(expression._FigureVisitName):
 
     def __call__(self, name, metadata, *args, **kwargs):
         schema = kwargs.get('schema', None)
-        autoload = kwargs.pop('autoload', False)
-        autoload_with = kwargs.pop('autoload_with', False)
-        mustexist = kwargs.pop('mustexist', False)
         useexisting = kwargs.pop('useexisting', False)
-        include_columns = kwargs.pop('include_columns', None)
+        mustexist = kwargs.pop('mustexist', False)
         key = _get_table_key(name, schema)
         try:
             table = metadata.tables[key]
@@ -101,57 +98,23 @@ class _TableSingleton(expression._FigureVisitName):
         except KeyError:
             if mustexist:
                 raise exceptions.ArgumentError("Table '%s' not defined" % (key))
-            table = type.__call__(self, name, metadata, **kwargs)
-            table._set_parent(metadata)
-            # load column definitions from the database if 'autoload' is defined
-            # we do it after the table is in the singleton dictionary to support
-            # circular foreign keys
-            if autoload:
-                try:
-                    if autoload_with:
-                        autoload_with.reflecttable(table, include_columns=include_columns)
-                    else:
-                        metadata._get_bind(raiseerr=True).reflecttable(table, include_columns=include_columns)
-                except exceptions.NoSuchTableError:
+            try:
+                return type.__call__(self, name, metadata, *args, **kwargs)
+            except:
+                if key in metadata.tables:
                     del metadata.tables[key]
-                    raise
-            # initialize all the column, etc. objects.  done after
-            # reflection to allow user-overrides
-            table._init_items(*args)
-            return table
+                raise
 
 
 class Table(SchemaItem, expression.TableClause):
-    """Represent a relational database table.
-
-    This subclasses ``expression.TableClause`` to provide a table that is
-    associated with an instance of ``MetaData``, which in turn
-    may be associated with an instance of ``Engine``.  
-
-    Whereas ``TableClause`` represents a table as its used in an SQL
-    expression, ``Table`` represents a table as it exists in a
-    database schema.
-    
-    If this ``Table`` is ultimately associated with an engine,
-    the ``Table`` gains the ability to access the database directly
-    without the need for dealing with an explicit ``Connection`` object;
-    this is known as "implicit execution".
-    
-    Implicit operation allows the ``Table`` to access the database to
-    reflect its own properties (via the autoload=True flag), it allows
-    the create() and drop() methods to be called without passing
-    a connectable, and it also propigates the underlying engine to
-    constructed SQL objects so that they too can be executed via their
-    execute() method without the need for a ``Connection``.
-    """
+    """Represent a relational database table."""
 
     __metaclass__ = _TableSingleton
 
-    def __init__(self, name, metadata, **kwargs):
+    def __init__(self, name, metadata, *args, **kwargs):
         """Construct a Table.
 
-        Table objects can be constructed directly.  The init method is
-        actually called via the TableSingleton metaclass.  Arguments
+        Table objects can be constructed directly.  Arguments
         are:
 
         name
@@ -168,8 +131,8 @@ class Table(SchemaItem, expression.TableClause):
           Should contain a listing of the Column objects for this table.
 
         \**kwargs
-          Options include:
-
+          kwargs include:
+          
           schema
             The *schema name* for this table, which is
             required if the table resides in a schema other than the
@@ -185,14 +148,14 @@ class Table(SchemaItem, expression.TableClause):
             if autoload==True, this is an optional Engine or Connection
             instance to be used for the table reflection.  If ``None``,
             the underlying MetaData's bound connectable will be used.
-            
+        
           include_columns
             A list of strings indicating a subset of columns to be 
             loaded via the ``autoload`` operation; table columns who
             aren't present in this list will not be represented on the resulting
             ``Table`` object.  Defaults to ``None`` which indicates all 
             columns should be reflected.
-            
+        
           mustexist
             Defaults to False: indicates that this Table must already
             have been defined elsewhere in the application, else an
@@ -236,18 +199,35 @@ class Table(SchemaItem, expression.TableClause):
         else:
             self.fullname = self.name
         self.owner = kwargs.pop('owner', None)
+        
+        autoload = kwargs.pop('autoload', False)
+        autoload_with = kwargs.pop('autoload_with', None)
+        include_columns = kwargs.pop('include_columns', None)
 
+        # validate remaining kwargs that they all specify DB prefixes
         if len([k for k in kwargs if not re.match(r'^(?:%s)_' % '|'.join(databases.__all__), k)]):
             raise TypeError("Invalid argument(s) for Table: %s" % repr(kwargs.keys()))
 
-        # store extra kwargs, which should only contain db-specific options
         self.kwargs = kwargs
+        
+        self._set_parent(metadata)
+        # load column definitions from the database if 'autoload' is defined
+        # we do it after the table is in the singleton dictionary to support
+        # circular foreign keys
+        if autoload:
+            if autoload_with:
+                autoload_with.reflecttable(self, include_columns=include_columns)
+            else:
+                metadata._get_bind(raiseerr=True).reflecttable(self, include_columns=include_columns)
+                
+        # initialize all the column, etc. objects.  done after
+        # reflection to allow user-overrides
+        self._init_items(*args)
 
     key = property(lambda self:_get_table_key(self.name, self.schema))
     
     def _export_columns(self, columns=None):
-        # override FromClause's collection initialization logic; TableClause and Table
-        # implement it differently
+        # override FromClause's collection initialization logic; Table implements it differently
         pass
 
     def _set_primary_key(self, pk):
@@ -510,6 +490,7 @@ class Column(SchemaItem, expression._ColumnClause):
             table._columns.add(self)
         else:
             self._pre_existing_column = None
+            
         if self.primary_key:
             table.primary_key.add(self)
         elif self.key in table.primary_key:
@@ -632,8 +613,10 @@ class ForeignKey(SchemaItem):
                 # locate the parent table this foreign key is attached to.
                 # we use the "original" column which our parent column represents
                 # (its a list of columns/other ColumnElements if the parent table is a UNION)
-                if isinstance(self.parent.base_column, Column):
-                    parenttable = self.parent.base_column.table
+                for c in self.parent.base_columns:
+                    if isinstance(c, Column):
+                        parenttable = c.table
+                        break
                 else:
                     raise exceptions.ArgumentError("Parent column '%s' does not descend from a table-attached Column" % str(self.parent))
                 m = re.match(r"^(.+?)(?:\.(.+?))?(?:\.(.+?))?$", self._colspec, re.UNICODE)
index 51bd176c3783514c4f2ce05025c361f7743f8df4..53364ba52dee863f68aa60a45e5fa6c33906f18e 100644 (file)
@@ -1393,15 +1393,13 @@ class ColumnElement(ClauseElement, _CompareMixin):
 
     foreign_key = property(_one_fkey)
     
-    def base_column(self):
-        if hasattr(self, '_base_column'):
-            return self._base_column
-        p = self
-        while hasattr(p, 'proxies'):
-            p = p.proxies[0]
-        self._base_column = p
-        return p
-    base_column = property(base_column)
+    def base_columns(self):
+        if hasattr(self, '_base_columns'):
+            return self._base_columns
+        self._base_columns = util.Set([c for c in self.proxy_set if not hasattr(c, 'proxies')])
+        return self._base_columns
+
+    base_columns = property(base_columns)
     
     def proxy_set(self):
         if hasattr(self, '_proxy_set'):
@@ -1578,7 +1576,7 @@ class FromClause(Selectable):
       from sqlalchemy.sql import util
       return util.ClauseAdapter(alias).traverse(self, clone=True)
 
-    def corresponding_column(self, column, raiseerr=True, keys_ok=False, require_embedded=False):
+    def corresponding_column(self, column, raiseerr=True, require_embedded=False):
         """Given a ``ColumnElement``, return the exported ``ColumnElement``
         object from this ``Selectable`` which corresponds to that
         original ``Column`` via a common anscestor column.
@@ -1590,11 +1588,6 @@ class FromClause(Selectable):
           if True, raise an error if the given ``ColumnElement`` could
           not be matched. if False, non-matches will return None.
 
-        keys_ok
-          if the ``ColumnElement`` cannot be matched, attempt to match
-          based on the string "key" property of the column alone. This
-          makes the search much more liberal.
-
         require_embedded
           only return corresponding columns for the given
           ``ColumnElement``, if the given ``ColumnElement`` is
@@ -1618,18 +1611,17 @@ class FromClause(Selectable):
                 col, intersect = c, i
         if col:
             return col
+
+        if not raiseerr:
+            return None
         else:
-            if keys_ok:
-                try:
-                    return self.c[column.name]
-                except KeyError:
-                    pass
-            if not raiseerr:
-                return None
-            else:
-                raise exceptions.InvalidRequestError("Given column '%s', attached to table '%s', failed to locate a corresponding column from table '%s'" % (str(column), str(getattr(column, 'table', None)), self.description))
+            raise exceptions.InvalidRequestError("Given column '%s', attached to table '%s', failed to locate a corresponding column from table '%s'" % (str(column), str(getattr(column, 'table', None)), self.description))
 
     def description(self):
+        """a brief description of this FromClause.
+        
+        Used primarily for error message formatting.
+        """
         return getattr(self, 'name', self.__class__.__name__ + " object")
     description = property(description)
     
@@ -1669,21 +1661,19 @@ class FromClause(Selectable):
     foreign_keys = property(_expr_attr_func('_foreign_keys'))
 
     def _export_columns(self, columns=None):
-        """Initialize column collections.
-
-        """
+        """Initialize column collections."""
 
         if hasattr(self, '_columns') and columns is None:
             return
         self._columns = ColumnCollection()
         self._primary_key = ColumnSet()
         self._foreign_keys = util.Set()
-
+        
         if columns is None:
             columns = self._flatten_exportable_columns()
         for co in columns:
             cp = self._proxy_column(co)
-
+            
     def _flatten_exportable_columns(self):
         """Return the list of ColumnElements represented within this FromClause's _exportable_columns"""
         export = self._exportable_columns()
@@ -2512,7 +2502,7 @@ class _Label(ColumnElement):
     key = property(lambda s: s.name)
     _label = property(lambda s: s.name)
     proxies = property(lambda s:s.obj.proxies)
-    base_column = property(lambda s:s.obj.base_column)
+    base_columns = property(lambda s:s.obj.base_columns)
     proxy_set = property(lambda s:s.obj.proxy_set)
     
     def expression_element(self):
index eed06cfc32b7bba60247af6b095b1c5302c92934..ecf4f3c16311117298131089a387543fbbdef68a 100644 (file)
@@ -235,10 +235,10 @@ class ClauseAdapter(AbstractClauseProcessor):
         if self.exclude is not None:
             if col in self.exclude:
                 return None
-        newcol = self.selectable.corresponding_column(col, raiseerr=False, require_embedded=True, keys_ok=False)
+        newcol = self.selectable.corresponding_column(col, raiseerr=False, require_embedded=True)
         if newcol is None and self.equivalents is not None and col in self.equivalents:
             for equiv in self.equivalents[col]:
-                newcol = self.selectable.corresponding_column(equiv, raiseerr=False, require_embedded=True, keys_ok=False)
+                newcol = self.selectable.corresponding_column(equiv, raiseerr=False, require_embedded=True)
                 if newcol:
                     return newcol
         return newcol
index 9ad7e113c41699e72158db13130bf9116758d891..e8c34b513e8f63c52817c9ff77e218cf35a90505 100644 (file)
@@ -409,27 +409,6 @@ except ImportError:
             def __setattr__(self, key, value):
                 self._tdict[(thread.get_ident(), key)] = value
 
-class DictDecorator(dict):
-    """A Dictionary that delegates items not found to a second wrapped dictionary."""
-
-    def __init__(self, decorate):
-        self.decorate = decorate
-
-    def __getitem__(self, key):
-        try:
-            return dict.__getitem__(self, key)
-        except KeyError:
-            return self.decorate[key]
-
-    def __contains__(self, key):
-        return dict.__contains__(self, key) or key in self.decorate
-
-    def has_key(self, key):
-        return key in self
-
-    def __repr__(self):
-        return dict.__repr__(self) + repr(self.decorate)
-
 class OrderedSet(Set):
     def __init__(self, d=None):
         Set.__init__(self)
index b37c985a60a38303f0528f84af1fe8b7328ed7e3..4402fe48f0b060a02bf0343dae6fd860bef67a0b 100644 (file)
@@ -37,7 +37,15 @@ class MapperTest(MapperSuperTest):
             assert False
         except exceptions.ArgumentError:
             pass
-
+    
+    def test_prop_accessor(self):
+        mapper(User, users)
+        try:
+            class_mapper(User).properties
+            assert False
+        except NotImplementedError, uoe:
+            assert str(uoe) == "Public collection of MapperProperty objects is provided by the get_property() and iterate_properties accessors."
+            
     def test_badcascade(self):
         mapper(Address, addresses)
         try: