--- /dev/null
+.. change::
+ :tags: feature, orm
+ :tickets: 4397
+
+ Added new :meth:`.MapperEvents.before_mapper_configured` event. This
+ event complements the other "configure" stage mapper events with a per
+ mapper event that receives each :class:`.Mapper` right before its
+ configure step, and additionally may be used to prevent or delay the
+ configuration of specific :class:`.Mapper` objects using a new
+ return value :attr:`.orm.interfaces.EXT_SKIP`. See the
+ documentation link for an example.
+
+ .. seealso::
+
+ :ref:`.MapperEvents.before_mapper_configured`
+
+
return self._key in _key_to_collection
def base_listen(self, propagate=False, insert=False,
- named=False):
+ named=False, retval=None):
target, identifier, fn = \
self.dispatch_target, self.identifier, self._listen_fn
from .interfaces import (
EXT_CONTINUE,
EXT_STOP,
+ EXT_SKIP,
PropComparator,
)
from .deprecated_interfaces import (
EXT_CONTINUE = util.symbol('EXT_CONTINUE')
EXT_STOP = util.symbol('EXT_STOP')
+EXT_SKIP = util.symbol('EXT_SKIP')
ONETOMANY = util.symbol(
'ONETOMANY',
_dispatch_target = None
@classmethod
- def _listen(cls, event_key, raw=False, propagate=False, **kw):
+ def _listen(
+ cls, event_key, raw=False, propagate=False,
+ retval=False, **kw):
target, identifier, fn = \
event_key.dispatch_target, event_key.identifier, event_key.fn
collection = target.all_holds[target.class_] = {}
event.registry._stored_in_collection(event_key, target)
- collection[event_key._key] = (event_key, raw, propagate)
+ collection[event_key._key] = (event_key, raw, propagate, retval)
if propagate:
stack = list(target.class_.__subclasses__())
# we are already going through __subclasses__()
# so leave generic propagate flag False
event_key.with_dispatch_target(subject).\
- listen(raw=raw, propagate=False, **kw)
+ listen(raw=raw, propagate=False, retval=retval, **kw)
def remove(self, event_key):
target, identifier, fn = \
for subclass in class_.__mro__:
if subclass in cls.all_holds:
collection = cls.all_holds[subclass]
- for event_key, raw, propagate in collection.values():
+ for event_key, raw, propagate, retval in collection.values():
if propagate or subclass is class_:
# since we can't be sure in what order different
# classes in a hierarchy are triggered with
# assignment, instead of using the generic propagate
# flag.
event_key.with_dispatch_target(subject).\
- listen(raw=raw, propagate=False)
+ listen(raw=raw, propagate=False, retval=retval)
class _InstanceEventsHold(_EventsHold):
"""
+ def before_mapper_configured(self, mapper, class_):
+ """Called right before a specific mapper is to be configured.
+
+ This event is intended to allow a specific mapper to be skipped during
+ the configure step, by returning the :attr:`.orm.interfaces.EXT_SKIP`
+ symbol which indicates to the :func:`.configure_mappers` call that this
+ particular mapper (or hierarchy of mappers, if ``propagate=True`` is
+ used) should be skipped in the current configuration run. When one or
+ more mappers are skipped, the he "new mappers" flag will remain set,
+ meaning the :func:`.configure_mappers` function will continue to be
+ called when mappers are used, to continue to try to configure all
+ available mappers.
+
+ In comparison to the other configure-level events,
+ :meth:`.MapperEvents.before_configured`,
+ :meth:`.MapperEvents.after_configured`, and
+ :meth:`.MapperEvents.mapper_configured`, the
+ :meth;`.MapperEvents.before_mapper_configured` event provides for a
+ meaningful return value when it is registered with the ``retval=True``
+ parameter.
+
+ .. versionadded:: 1.3
+
+ e.g.::
+
+ from sqlalchemy.orm import EXT_SKIP
+
+ Base = declarative_base()
+
+ DontConfigureBase = declarative_base()
+
+ @event.listens_for(
+ DontConfigureBase,
+ "before_mapper_configured", retval=True, propagate=True)
+ def dont_configure(mapper, cls):
+ return EXT_SKIP
+
+
+ .. seealso::
+
+ :meth:`.MapperEvents.before_configured`
+
+ :meth:`.MapperEvents.after_configured`
+
+ :meth:`.MapperEvents.mapper_configured`
+
+ """
+
def mapper_configured(self, mapper, class_):
r"""Called when a specific mapper has completed its own configuration
within the scope of the :func:`.configure_mappers` call.
:meth:`.MapperEvents.after_configured`
+ :meth:`.MapperEvents.before_mapper_configured`
+
"""
# TODO: need coverage for this event
Contrast this event to :meth:`.MapperEvents.after_configured`,
which is invoked after the series of mappers has been configured,
- as well as :meth:`.MapperEvents.mapper_configured`, which is invoked
- on a per-mapper basis as each one is configured to the extent possible.
+ as well as :meth:`.MapperEvents.before_mapper_configured`
+ and :meth:`.MapperEvents.mapper_configured`, which are both invoked
+ on a per-mapper basis.
Theoretically this event is called once per
application, but is actually called any time new mappers
.. seealso::
+ :meth:`.MapperEvents.before_mapper_configured`
+
:meth:`.MapperEvents.mapper_configured`
:meth:`.MapperEvents.after_configured`
.. seealso::
+ :meth:`.MapperEvents.before_mapper_configured`
+
:meth:`.MapperEvents.mapper_configured`
:meth:`.MapperEvents.before_configured`
from .. import util
from ..sql import operators
from .base import (ONETOMANY, MANYTOONE, MANYTOMANY,
- EXT_CONTINUE, EXT_STOP, NOT_EXTENSION)
+ EXT_CONTINUE, EXT_STOP, EXT_SKIP, NOT_EXTENSION)
from .base import InspectionAttr, InspectionAttrInfo, _MappedAttribute
import collections
from .. import inspect
'AttributeExtension',
'EXT_CONTINUE',
'EXT_STOP',
+ 'EXT_SKIP',
'ONETOMANY',
'MANYTOMANY',
'MANYTOONE',
from . import instrumentation, attributes, exc as orm_exc, loading
from . import properties
from . import util as orm_util
-from .interfaces import MapperProperty, InspectionAttr, _MappedAttribute
+from .interfaces import MapperProperty, InspectionAttr, _MappedAttribute, \
+ EXT_SKIP
+
from .base import _class_to_mapper, _state_mapper, class_mapper, \
state_str, _INSTRUMENTOR
if not Mapper._new_mappers:
return
+ has_skip = False
+
Mapper.dispatch._for_class(Mapper).before_configured()
# initialize properties on all mappers
# note that _mapper_registry is unordered, which
# the order of mapper compilation
for mapper in list(_mapper_registry):
+ run_configure = None
+ for fn in mapper.dispatch.before_mapper_configured:
+ run_configure = fn(mapper, mapper.class_)
+ if run_configure is EXT_SKIP:
+ has_skip = True
+ break
+ if run_configure is EXT_SKIP:
+ continue
+
if getattr(mapper, '_configure_failed', False):
e = sa_exc.InvalidRequestError(
"One or more mappers failed to initialize - "
% (mapper, mapper._configure_failed))
e._configure_failed = mapper._configure_failed
raise e
+
if not mapper.configured:
try:
mapper._post_configure_properties()
mapper._configure_failed = exc
raise
- Mapper._new_mappers = False
+ if not has_skip:
+ Mapper._new_mappers = False
finally:
_already_compiling = False
finally:
import sqlalchemy as sa
from sqlalchemy import testing
from sqlalchemy import Integer, String
+from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.testing.schema import Table, Column
from sqlalchemy.orm import mapper, relationship, \
create_session, class_mapper, \
Session, sessionmaker, attributes, configure_mappers
from sqlalchemy.orm.instrumentation import ClassManager
from sqlalchemy.orm import instrumentation, events
-from sqlalchemy.testing import eq_, is_not_
+from sqlalchemy.orm import EXT_SKIP
+from sqlalchemy.orm.mapper import _mapper_registry
+from sqlalchemy.testing import eq_, is_not_, assert_raises
from sqlalchemy.testing import fixtures
from sqlalchemy.testing import AssertsCompiledSQL
from sqlalchemy.testing.util import gc_collect
canary.mock_calls
)
+ def test_before_mapper_configured_event(self):
+ """Test [ticket:4397].
+
+ This event is intended to allow a specific mapper to be skipped during
+ the configure step, by returning a value of
+ :attr:`.orm.interfaces.EXT_SKIP` which means the mapper will be skipped
+ within this configure run. The "new mappers" flag will remain set in
+ this case and the configure operation will occur again.
+
+ This event, and its return value, make it possible to query one base
+ while a different one still needs configuration, which cannot be
+ completed at this time.
+ """
+
+ User, users = self.classes.User, self.tables.users
+ mapper(User, users)
+
+ AnotherBase = declarative_base()
+
+ class Animal(AnotherBase):
+ __tablename__ = 'animal'
+ species = Column(String(30), primary_key=True)
+ __mapper_args__ = dict(
+ polymorphic_on='species', polymorphic_identity='Animal')
+
+ # Register the first classes and create their Mappers:
+ configure_mappers()
+
+ unconfigured = [m for m in _mapper_registry if not m.configured]
+ eq_(0, len(unconfigured))
+
+ # Declare a subclass, table and mapper, which refers to one that has
+ # not been loaded yet (Employer), and therefore cannot be configured:
+ class Mammal(Animal):
+ nonexistent = relationship('Nonexistent')
+
+ # These new classes should not be configured at this point:
+ unconfigured = [m for m in _mapper_registry if not m.configured]
+ eq_(1, len(unconfigured))
+
+ # Now try to query User, which is internally consistent. This query
+ # fails by default because Mammal needs to be configured, and cannot
+ # be:
+ def probe():
+ s = Session()
+ s.query(User)
+ assert_raises(sa.exc.InvalidRequestError, probe)
+
+ # If we disable configuring mappers while querying, then it succeeds:
+ @event.listens_for(
+ AnotherBase, "before_mapper_configured", propagate=True,
+ retval=True)
+ def disable_configure_mappers(mapper, cls):
+ return EXT_SKIP
+
+ probe()
+
class DeclarativeEventListenTest(_RemoveListeners,
fixtures.DeclarativeMappedTest):