From: Mike Bayer Date: Wed, 24 Dec 2008 04:47:06 +0000 (+0000) Subject: - Exceptions raised during compile_mappers() are now X-Git-Tag: rel_0_5_0~70 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=2876c7e46f5385441188781794b9088e4822999b;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - 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. --- diff --git a/CHANGES b/CHANGES index c278e62d17..84e94de1a4 100644 --- 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] diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index ca6dec6895..c3ff4e47c2 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -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`. diff --git a/test/ext/declarative.py b/test/ext/declarative.py index 1388456655..748ee7f854 100644 --- a/test/ext/declarative.py +++ b/test/ext/declarative.py @@ -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): diff --git a/test/orm/mapper.py b/test/orm/mapper.py index e30b534281..64a329f842 100644 --- a/test/orm/mapper.py +++ b/test/orm/mapper.py @@ -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={