]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Exceptions raised during compile_mappers() are now
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 24 Dec 2008 04:47:06 +0000 (04:47 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 24 Dec 2008 04:47:06 +0000 (04:47 +0000)
preserved to provide "sticky behavior" - if a hasattr()
call on a pre-compiled mapped attribute triggers a failing
compile and suppresses the exception, subsequent compilation
is blocked and the exception will be reiterated on the
next compile() call.  This issue occurs frequently
when using declarative.

CHANGES
lib/sqlalchemy/orm/mapper.py
test/ext/declarative.py
test/orm/mapper.py

diff --git a/CHANGES b/CHANGES
index c278e62d17a72224b92a913ef9571003984454dc..84e94de1a4811f4bc77760631eff7152f75118d6 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -44,6 +44,14 @@ CHANGES
 - bugfixes, behavioral changes
 - general
 - orm
+    - Exceptions raised during compile_mappers() are now
+      preserved to provide "sticky behavior" - if a hasattr()
+      call on a pre-compiled mapped attribute triggers a failing 
+      compile and suppresses the exception, subsequent compilation 
+      is blocked and the exception will be reiterated on the 
+      next compile() call.  This issue occurs frequently
+      when using declarative.
+      
     - Corrected problem with Query.delete() and
       Query.update() not working properly with bind 
       parameters. [ticket:1242]
index ca6dec6895b72d14ec880b75740dedc438803965..c3ff4e47c26274ed9032abc09ff57de77fc9ee1a 100644 (file)
@@ -389,7 +389,8 @@ class Mapper(object):
         # Disable any attribute-based compilation.
         self.compiled = True
         manager = self.class_manager
-
+        if hasattr(self, '_compile_failed'):
+            del self._compile_failed
         if not self.non_primary and manager.mapper is self:
             manager.mapper = None
             manager.events.remove_listener('on_init', _event_on_init)
@@ -532,7 +533,7 @@ class Mapper(object):
                 return object.__getattribute__(self, key)
 
             class_mapper(cls)
-
+                
             if cls.__dict__.get(clskey) is self:
                 # if this warning occurs, it usually means mapper
                 # compilation has failed, but operations upon the mapped
@@ -659,29 +660,39 @@ class Mapper(object):
 
         _COMPILE_MUTEX.acquire()
         try:
-            global _already_compiling
-            if _already_compiling:
-                # re-entrance to compile() occurs rarely, when a class-mapped construct is
-                # used within a ForeignKey, something that is possible
-                # when using the declarative layer
-                self._post_configure_properties()
-                return
-            _already_compiling = True
             try:
-
-                # double-check inside mutex
-                if self.compiled and not _new_mappers:
+                global _already_compiling
+                if _already_compiling:
+                    # re-entrance to compile() occurs rarely, when a class-mapped construct is
+                    # used within a ForeignKey, something that is possible
+                    # when using the declarative layer
+                    self._post_configure_properties()
+                    return
+                _already_compiling = True
+                try:
+
+                    # double-check inside mutex
+                    if self.compiled and not _new_mappers:
+                        return self
+
+                    # initialize properties on all mappers
+                    for mapper in list(_mapper_registry):
+                        if getattr(mapper, '_compile_failed', False):
+                            raise sa_exc.InvalidRequestError("One or more mappers failed to compile.  Exception was probably "
+                                    "suppressed within a hasattr() call. "
+                                    "Message was: %s" % mapper._compile_failed)
+                        if not mapper.compiled:
+                            mapper._post_configure_properties()
+
+                    _new_mappers = False
                     return self
-
-                # initialize properties on all mappers
-                for mapper in list(_mapper_registry):
-                    if not mapper.compiled:
-                        mapper._post_configure_properties()
-
-                _new_mappers = False
-                return self
-            finally:
-                _already_compiling = False
+                finally:
+                    _already_compiling = False
+            except:
+                import sys
+                exc = sys.exc_info()[1]
+                self._compile_failed = exc
+                raise
         finally:
             _COMPILE_MUTEX.release()
 
@@ -693,6 +704,7 @@ class Mapper(object):
         to execute once all mappers have been constructed.
         
         """
+
         self._log("_post_configure_properties() started")
         l = [(key, prop) for key, prop in self._props.iteritems()]
         for key, prop in l:
@@ -701,7 +713,7 @@ class Mapper(object):
                 prop.init(key, self)
         self._log("_post_configure_properties() complete")
         self.compiled = True
-
+            
     def add_properties(self, dict_of_properties):
         """Add the given dictionary of properties to this mapper,
         using `add_property`.
index 1388456655c123b01237c0fec7cc239e1852b33d..748ee7f8549afafb4be787d741771c7f70751872 100644 (file)
@@ -4,7 +4,7 @@ from sqlalchemy.ext import declarative as decl
 from sqlalchemy import exc
 from testlib import sa, testing
 from testlib.sa import MetaData, Table, Column, Integer, String, ForeignKey, ForeignKeyConstraint, asc
-from testlib.sa.orm import relation, create_session, class_mapper, eagerload, compile_mappers, backref
+from testlib.sa.orm import relation, create_session, class_mapper, eagerload, compile_mappers, backref, clear_mappers
 from testlib.testing import eq_
 from orm._base import ComparableEntity, MappedTest
 
@@ -15,6 +15,7 @@ class DeclarativeTest(testing.TestBase, testing.AssertsExecutionResults):
         Base = decl.declarative_base(testing.db)
 
     def tearDown(self):
+        clear_mappers()
         Base.metadata.drop_all()
 
     def test_basic(self):
@@ -185,6 +186,20 @@ class DeclarativeTest(testing.TestBase, testing.AssertsExecutionResults):
                 foo = sa.orm.column_property(User.id == 5)
         self.assertRaises(sa.exc.InvalidRequestError, go)
 
+    def test_nice_dependency_error_works_with_hasattr(self):
+        class User(Base):
+            __tablename__ = 'users'
+            id = Column('id', Integer, primary_key=True)
+            addresses = relation("Addresss")
+
+        # doesn't raise, hasattr() squashes all exceptions 
+        # (except KeybaordInterrupt/SystemException in 2.6...whoopee)
+        # TODO: determine what hasattr() does on py3K
+        hasattr(User.id, 'in_')
+        # but the exception is saved, compile_mappers tells us what it is 
+        # as well as some explaination
+        self.assertRaisesMessage(sa.exc.InvalidRequestError, r"suppressed within a hasattr\(\)", compile_mappers)
+
     def test_custom_base(self):
         class MyBase(object):
             def foobar(self):
index e30b534281bfddbd95af23d4cf5508c7a4c7fdab..64a329f8420259266d60052b50b1dd22e7e7cdcf 100644 (file)
@@ -4,7 +4,7 @@ import testenv; testenv.configure_for_tests()
 from testlib import sa, testing
 from testlib.sa import MetaData, Table, Column, Integer, String, ForeignKey, func
 from testlib.sa.engine import default
-from testlib.sa.orm import mapper, relation, backref, create_session, class_mapper, reconstructor, validates, aliased
+from testlib.sa.orm import mapper, relation, backref, create_session, class_mapper, compile_mappers, reconstructor, validates, aliased
 from testlib.sa.orm import defer, deferred, synonym, attributes, column_property, composite, relation, dynamic_loader, comparable_property
 from testlib.testing import eq_, AssertsCompiledSQL
 import pickleable
@@ -37,6 +37,14 @@ class MapperTest(_fixtures.FixtureTest):
         self.assertRaises(sa.exc.ArgumentError,
                           relation, Address, cascade="fake, all, delete-orphan")
 
+    @testing.resolve_artifact_names
+    def test_exceptions_sticky(self):
+        mapper(Address, addresses, properties={
+            'user':relation(User)
+        })
+        hasattr(Address.id, 'in_')
+        self.assertRaisesMessage(sa.exc.InvalidRequestError, r"suppressed within a hasattr\(\)", compile_mappers)
+    
     @testing.resolve_artifact_names
     def test_column_prefix(self):
         mapper(User, users, column_prefix='_', properties={