--- /dev/null
+.. change::
+ :tags: changed, orm
+ :tickets: 5897
+
+ Mapper "configuration", which occurs within the
+ :func:`_orm.configure_mappers` function, is now organized to be on a
+ per-registry basis. This allows for example the mappers within a certain
+ declarative base to be configured, but not those of another base that is
+ also present in memory. The goal is to provide a means of reducing
+ application startup time by only running the "configure" process for sets
+ of mappers that are needed. This also adds the
+ :meth:`_orm.registry.configure` method that will run configure for the
+ mappers local in a particular registry only.
from .interfaces import PropComparator # noqa
from .loading import merge_frozen_result # noqa
from .loading import merge_result # noqa
-from .mapper import _mapper_registry
from .mapper import class_mapper # noqa
from .mapper import configure_mappers # noqa
from .mapper import Mapper # noqa
def clear_mappers():
"""Remove all mappers from all classes.
+ .. versionchanged:: 1.4 This function now locates all
+ :class:`_orm.registry` objects and calls upon the
+ :meth:`_orm.registry.dispose` method of each.
+
This function removes all instrumentation from classes and disposes
of their associated mappers. Once called, the classes are unmapped
and can be later re-mapped with new mappers.
"""
with mapperlib._CONFIGURE_MUTEX:
- while _mapper_registry:
- try:
- mapper, b = _mapper_registry.popitem()
- except KeyError:
- # weak registry, item could have been collected
- pass
- else:
- mapper.dispose()
+ all_regs = mapperlib._all_registries()
+ mapperlib._dispose_registries(all_regs, False)
joinedload = strategy_options.joinedload._unbound_fn
DEFAULT_MANAGER_ATTR = "_sa_class_manager"
DEFAULT_STATE_ATTR = "_sa_instance_state"
-_INSTRUMENTOR = ("mapper", "instrumentor")
EXT_CONTINUE = util.symbol("EXT_CONTINUE")
EXT_STOP = util.symbol("EXT_STOP")
except exc.NO_STATE:
return None
else:
- if configure and mapper._new_mappers:
- mapper._configure_all()
+ if configure:
+ mapper._check_configure()
return mapper
"""Public API functions and helpers for declarative."""
from __future__ import absolute_import
+import itertools
import re
import weakref
from . import attributes
from . import clsregistry
from . import exc as orm_exc
+from . import instrumentation
from . import interfaces
+from . import mapper as mapperlib
from .base import _inspect_mapped_class
from .decl_base import _add_attribute
from .decl_base import _as_declarative
class_registry = weakref.WeakValueDictionary()
self._class_registry = class_registry
+ self._managers = weakref.WeakKeyDictionary()
+ self._non_primary_mappers = weakref.WeakKeyDictionary()
self.metadata = lcl_metadata
self.constructor = constructor
+ self._dependents = set()
+ self._dependencies = set()
+
+ self._new_mappers = False
+
+ mapperlib._mapper_registries[self] = True
+
+ @property
+ def mappers(self):
+ """read only collection of all :class:`_orm.Mapper` objects."""
+
+ return frozenset(manager.mapper for manager in self._managers).union(
+ self._non_primary_mappers
+ )
+
+ def _set_depends_on(self, registry):
+ if registry is self:
+ return
+ registry._dependents.add(self)
+ self._dependencies.add(registry)
+
+ def _flag_new_mapper(self, mapper):
+ mapper._ready_for_configure = True
+ if self._new_mappers:
+ return
+
+ for reg in self._recurse_with_dependents({self}):
+ reg._new_mappers = True
+
+ @classmethod
+ def _recurse_with_dependents(cls, registries):
+ todo = registries
+ done = set()
+ while todo:
+ reg = todo.pop()
+ done.add(reg)
+
+ # if yielding would remove dependents, make sure we have
+ # them before
+ todo.update(reg._dependents.difference(done))
+ yield reg
+
+ # if yielding would add dependents, make sure we have them
+ # after
+ todo.update(reg._dependents.difference(done))
+
+ @classmethod
+ def _recurse_with_dependencies(cls, registries):
+ todo = registries
+ done = set()
+ while todo:
+ reg = todo.pop()
+ done.add(reg)
+
+ # if yielding would remove dependencies, make sure we have
+ # them before
+ todo.update(reg._dependencies.difference(done))
+
+ yield reg
+
+ # if yielding would remove dependencies, make sure we have
+ # them before
+ todo.update(reg._dependencies.difference(done))
+
+ def _mappers_to_configure(self):
+ return itertools.chain(
+ (
+ manager.mapper
+ for manager in self._managers
+ if manager.is_mapped
+ and not manager.mapper.configured
+ and manager.mapper._ready_for_configure
+ ),
+ (
+ npm
+ for npm in self._non_primary_mappers
+ if not npm.configured and npm._ready_for_configure
+ ),
+ )
+
+ def _add_non_primary_mapper(self, np_mapper):
+ self._non_primary_mappers[np_mapper] = True
+
def _dispose_cls(self, cls):
clsregistry.remove_class(cls.__name__, cls, self._class_registry)
+ def _add_manager(self, manager):
+ self._managers[manager] = True
+ assert manager.registry is None
+ manager.registry = self
+
+ def configure(self, cascade=False):
+ """Configure all as-yet unconfigured mappers in this
+ :class:`_orm.registry`.
+
+ The configure step is used to reconcile and initialize the
+ :func:`_orm.relationship` linkages between mapped classes, as well as
+ to invoke configuration events such as the
+ :meth:`_orm.MapperEvents.before_configured` and
+ :meth:`_orm.MapperEvents.after_configured`, which may be used by ORM
+ extensions or user-defined extension hooks.
+
+ If one or more mappers in this registry contain
+ :func:`_orm.relationship` constructs that refer to mapped classes in
+ other registries, this registry is said to be *dependent* on those
+ registries. In order to configure those dependent registries
+ automatically, the :paramref:`_orm.registry.configure.cascade` flag
+ should be set to ``True``. Otherwise, if they are not configured, an
+ exception will be raised. The rationale behind this behavior is to
+ allow an application to programmatically invoke configuration of
+ registries while controlling whether or not the process implicitly
+ reaches other registries.
+
+ As an alternative to invoking :meth:`_orm.registry.configure`, the ORM
+ function :func:`_orm.configure_mappers` function may be used to ensure
+ configuration is complete for all :class:`_orm.registry` objects in
+ memory. This is generally simpler to use and also predates the usage of
+ :class:`_orm.registry` objects overall. However, this function will
+ impact all mappings throughout the running Python process and may be
+ more memory/time consuming for an application that has many registries
+ in use for different purposes that may not be needed immediately.
+
+ .. seealso::
+
+ :func:`_orm.configure_mappers`
+
+
+ .. versionadded:: 1.4.0b2
+
+ """
+ mapperlib._configure_registries({self}, cascade=cascade)
+
+ def dispose(self, cascade=False):
+ """Dispose of all mappers in this :class:`_orm.registry`.
+
+ After invocation, all the classes that were mapped within this registry
+ will no longer have class instrumentation associated with them. This
+ method is the per-:class:`_orm.registry` analogue to the
+ application-wide :func:`_orm.clear_mappers` function.
+
+ If this registry contains mappers that are dependencies of other
+ registries, typically via :func:`_orm.relationship` links, then those
+ registries must be disposed as well. When such registries exist in
+ relation to this one, their :meth:`_orm.registry.dispose` method will
+ also be called, if the :paramref:`_orm.registry.dispose.cascade` flag
+ is set to ``True``; otherwise, an error is raised if those registries
+ were not already disposed.
+
+ .. versionadded:: 1.4.0b2
+
+ .. seealso::
+
+ :func:`_orm.clear_mappers`
+
+ """
+
+ mapperlib._dispose_registries({self}, cascade=cascade)
+
+ def _dispose_manager_and_mapper(self, manager):
+ if "mapper" in manager.__dict__:
+ mapper = manager.mapper
+
+ mapper._set_dispose_flags()
+
+ class_ = manager.class_
+ self._dispose_cls(class_)
+ instrumentation._instrumentation_factory.unregister(class_)
+
def generate_base(
self,
mapper=None,
return _mapper(self, class_, local_table, kw)
+mapperlib._legacy_registry = registry()
+
+
@util.deprecated_params(
bind=(
"2.0",
if mapper:
self.mapper = mapper
if registry:
- self.registry = registry
+ registry._add_manager(self)
if declarative_scan:
self.declarative_scan = declarative_scan
if expired_attribute_loader:
setattr(self.class_, self.MANAGER_ATTR, self)
- def dispose(self):
- """Disassociate this manager from its class."""
-
- delattr(self.class_, self.MANAGER_ATTR)
-
@util.hybridmethod
def manager_getter(self):
return _default_manager_getter
if key in self.local_attrs:
self.uninstrument_attribute(key)
+ if self.MANAGER_ATTR in self.class_.__dict__:
+ delattr(self.class_, self.MANAGER_ATTR)
+
def install_descriptor(self, key, inst):
if key in (self.STATE_ATTR, self.MANAGER_ATTR):
raise KeyError(
"Python process!" % self.class_,
)
elif manager.is_mapped and not manager.mapper.configured:
- manager.mapper._configure_all()
+ manager.mapper._check_configure()
# setup _sa_instance_state ahead of time so that
# unpickle events can access the object normally.
def unregister(self, class_):
manager = manager_of_class(class_)
manager.unregister()
- manager.dispose()
self.dispatch.class_uninstrument(class_)
- if ClassManager.MANAGER_ATTR in class_.__dict__:
- delattr(class_, ClassManager.MANAGER_ATTR)
# this attribute is replaced by sqlalchemy.ext.instrumentation
relationships between mappers and perform other post-mapper-creation
initialization steps.
+
"""
self._configure_started = True
self.do_init()
from . import properties
from . import util as orm_util
from .base import _class_to_mapper
-from .base import _INSTRUMENTOR
from .base import _state_mapper
from .base import class_mapper
from .base import state_str
from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL
from ..util import HasMemoized
+_mapper_registries = weakref.WeakKeyDictionary()
+
+_legacy_registry = None
+
+
+def _all_registries():
+ return set(_mapper_registries)
+
+
+def _unconfigured_mappers():
+ for reg in _mapper_registries:
+ for mapper in reg._mappers_to_configure():
+ yield mapper
+
-_mapper_registry = weakref.WeakKeyDictionary()
_already_compiling = False
"""
- _new_mappers = False
_dispose_called = False
+ _ready_for_configure = False
@util.deprecated_params(
non_primary=(
techniques.
"""
-
self.class_ = util.assert_arg_type(class_, type, "class_")
self._sort_key = "%s.%s" % (
self.class_.__module__,
else None
)
self._dependency_processors = []
- self.validators = util.immutabledict()
+ self.validators = util.EMPTY_DICT
self.passive_updates = passive_updates
self.passive_deletes = passive_deletes
self.legacy_is_orphan = legacy_is_orphan
else:
self.exclude_properties = None
- self.configured = False
-
# prevent this mapper from being constructed
# while a configure_mappers() is occurring (and defer a
# configure_mappers() until construction succeeds)
self._configure_properties()
self._configure_polymorphic_setter()
self._configure_pks()
- Mapper._new_mappers = True
+ self.registry._flag_new_mapper(self)
self._log("constructed")
self._expire_memoizations()
"""
- configured = None
+ configured = False
"""Represent ``True`` if this :class:`_orm.Mapper` has been configured.
This is a *read only* attribute determined during mapper construction.
"Mapper." % self.class_
)
self.class_manager = manager
+ self.registry = manager.registry
self._identity_class = manager.mapper._identity_class
- _mapper_registry[self] = True
+ manager.registry._add_non_primary_mapper(self)
return
if manager is not None:
# ClassManager.instrument_attribute() creates
# new managers for each subclass if they don't yet exist.
- _mapper_registry[self] = True
-
self.dispatch.instrument_class(self, self.class_)
# this invokes the class_instrument event and sets up
# and call the class_instrument event
finalize=True,
)
+
if not manager.registry:
util.warn_deprecated_20(
"Calling the mapper() function directly outside of a "
" Please use the sqlalchemy.orm.registry.map_imperatively() "
"function for a classical mapping."
)
- from . import registry
-
- manager.registry = registry()
+ assert _legacy_registry is not None
+ _legacy_registry._add_manager(manager)
self.class_manager = manager
+ self.registry = manager.registry
# The remaining members can be added by any mapper,
# e_name None or not.
- if manager.info.get(_INSTRUMENTOR, False):
+ if manager.mapper is None:
return
- event.listen(manager, "first_init", _event_on_first_init, raw=True)
event.listen(manager, "init", _event_on_init, raw=True)
for key, method in util.iterate_attributes(self.class_):
{name: (method, validation_opts)}
)
- manager.info[_INSTRUMENTOR] = self
-
- @classmethod
- def _configure_all(cls):
- """Class-level path to the :func:`.configure_mappers` call."""
- configure_mappers()
-
- def dispose(self):
- # Disable any attribute-based compilation.
+ def _set_dispose_flags(self):
self.configured = True
+ self._ready_for_configure = True
self._dispose_called = True
- if hasattr(self, "_configure_failed"):
- del self._configure_failed
-
- if (
- not self.non_primary
- and self.class_manager is not None
- and self.class_manager.is_mapped
- and self.class_manager.mapper is self
- ):
- self.class_manager.registry._dispose_cls(self.class_)
- instrumentation.unregister_class(self.class_)
+ self.__dict__.pop("_configure_failed", None)
def _configure_pks(self):
self.tables = sql_util.find_tables(self.persist_selectable)
"columns get mapped." % (key, self, column.key, prop)
)
+ def _check_configure(self):
+ if self.registry._new_mappers:
+ _configure_registries({self.registry}, cascade=True)
+
def _post_configure_properties(self):
"""Call the ``init()`` method on all ``MapperProperties``
attached to this mapper.
def get_property(self, key, _configure_mappers=True):
"""return a MapperProperty associated with the given key."""
- if _configure_mappers and Mapper._new_mappers:
- configure_mappers()
+ if _configure_mappers:
+ self._check_configure()
try:
return self._props[key]
@property
def iterate_properties(self):
"""return an iterator of all MapperProperty objects."""
- if Mapper._new_mappers:
- configure_mappers()
+
+ self._check_configure()
return iter(self._props.values())
def _mappers_from_spec(self, spec, selectable):
@HasMemoized.memoized_attribute
def _with_polymorphic_mappers(self):
- if Mapper._new_mappers:
- configure_mappers()
+ self._check_configure()
+
if not self.with_polymorphic:
return []
return self._mappers_from_spec(*self.with_polymorphic)
This allows the inspection process run a configure mappers hook.
"""
- if Mapper._new_mappers:
- configure_mappers()
+ self._check_configure()
@HasMemoized.memoized_attribute
def _with_polymorphic_selectable(self):
:attr:`_orm.Mapper.all_orm_descriptors`
"""
- if Mapper._new_mappers:
- configure_mappers()
+
+ self._check_configure()
return util.ImmutableProperties(self._props)
@HasMemoized.memoized_attribute
)
def _filter_properties(self, type_):
- if Mapper._new_mappers:
- configure_mappers()
+ self._check_configure()
return util.ImmutableProperties(
util.OrderedDict(
(k, v) for k, v in self._props.items() if isinstance(v, type_)
def configure_mappers():
"""Initialize the inter-mapper relationships of all mappers that
- have been constructed thus far.
-
- This function can be called any number of times, but in
- most cases is invoked automatically, the first time mappings are used,
- as well as whenever mappings are used and additional not-yet-configured
- mappers have been constructed.
-
- Points at which this occur include when a mapped class is instantiated
- into an instance, as well as when the :meth:`.Session.query` method
- is used.
-
- The :func:`.configure_mappers` function provides several event hooks
- that can be used to augment its functionality. These methods include:
+ have been constructed thus far across all :class:`_orm.registry`
+ collections.
+
+ The configure step is used to reconcile and initialize the
+ :func:`_orm.relationship` linkages between mapped classes, as well as to
+ invoke configuration events such as the
+ :meth:`_orm.MapperEvents.before_configured` and
+ :meth:`_orm.MapperEvents.after_configured`, which may be used by ORM
+ extensions or user-defined extension hooks.
+
+ Mapper configuration is normally invoked automatically, the first time
+ mappings from a particular :class:`_orm.registry` are used, as well as
+ whenever mappings are used and additional not-yet-configured mappers have
+ been constructed. The automatic configuration process however is local only
+ to the :class:`_orm.registry` involving the target mapper and any related
+ :class:`_orm.registry` objects which it may depend on; this is
+ equivalent to invoking the :meth:`_orm.registry.configure` method
+ on a particular :class:`_orm.registry`.
+
+ By contrast, the :func:`_orm.configure_mappers` function will invoke the
+ configuration process on all :class:`_orm.registry` objects that
+ exist in memory, and may be useful for scenarios where many individual
+ :class:`_orm.registry` objects that are nonetheless interrelated are
+ in use.
+
+ .. versionchanged:: 1.4
+
+ As of SQLAlchemy 1.4.0b2, this function works on a
+ per-:class:`_orm.registry` basis, locating all :class:`_orm.registry`
+ objects present and invoking the :meth:`_orm.registry.configure` method
+ on each. The :meth:`_orm.registry.configure` method may be preferred to
+ limit the configuration of mappers to those local to a particular
+ :class:`_orm.registry` and/or declarative base class.
+
+ Points at which automatic configuration is invoked include when a mapped
+ class is instantiated into an instance, as well as when ORM queries
+ are emitted using :meth:`.Session.query` or :meth:`_orm.Session.execute`
+ with an ORM-enabled statement.
+
+ The mapper configure process, whether invoked by
+ :func:`_orm.configure_mappers` or from :meth:`_orm.registry.configure`,
+ provides several event hooks that can be used to augment the mapper
+ configuration step. These hooks include:
* :meth:`.MapperEvents.before_configured` - called once before
- :func:`.configure_mappers` does any work; this can be used to establish
- additional options, properties, or related mappings before the operation
- proceeds.
+ :func:`.configure_mappers` or :meth:`_orm.registry.configure` does any
+ work; this can be used to establish additional options, properties, or
+ related mappings before the operation proceeds.
* :meth:`.MapperEvents.mapper_configured` - called as each individual
:class:`_orm.Mapper` is configured within the process; will include all
to be configured.
* :meth:`.MapperEvents.after_configured` - called once after
- :func:`.configure_mappers` is complete; at this stage, all
- :class:`_orm.Mapper` objects that are known to SQLAlchemy will be fully
- configured. Note that the calling application may still have other
- mappings that haven't been produced yet, such as if they are in modules
- as yet unimported.
+ :func:`.configure_mappers` or :meth:`_orm.registry.configure` is
+ complete; at this stage, all :class:`_orm.Mapper` objects that fall
+ within the scope of the configuration operation will be fully configured.
+ Note that the calling application may still have other mappings that
+ haven't been produced yet, such as if they are in modules as yet
+ unimported, and may also have mappings that are still to be configured,
+ if they are in other :class:`_orm.registry` collections not part of the
+ current scope of configuration.
"""
- if not Mapper._new_mappers:
+ _configure_registries(set(_mapper_registries), cascade=True)
+
+
+def _configure_registries(registries, cascade):
+ for reg in registries:
+ if reg._new_mappers:
+ break
+ else:
return
with _CONFIGURE_MUTEX:
try:
# double-check inside mutex
- if not Mapper._new_mappers:
+ for reg in registries:
+ if reg._new_mappers:
+ break
+ else:
return
- has_skip = False
-
Mapper.dispatch._for_class(Mapper).before_configured()
# initialize properties on all mappers
# note that _mapper_registry is unordered, which
# may randomly conceal/reveal issues related to
# 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 - "
- "can't proceed with initialization of other "
- "mappers. Triggering mapper: '%s'. "
- "Original exception was: %s"
- % (mapper, mapper._configure_failed)
- )
- e._configure_failed = mapper._configure_failed
- raise e
-
- if not mapper.configured:
- try:
- mapper._post_configure_properties()
- mapper._expire_memoizations()
- mapper.dispatch.mapper_configured(
- mapper, mapper.class_
- )
- except Exception:
- exc = sys.exc_info()[1]
- if not hasattr(exc, "_configure_failed"):
- mapper._configure_failed = exc
- raise
-
- if not has_skip:
- Mapper._new_mappers = False
+ _do_configure_registries(registries, cascade)
finally:
_already_compiling = False
Mapper.dispatch._for_class(Mapper).after_configured()
+@util.preload_module("sqlalchemy.orm.decl_api")
+def _do_configure_registries(registries, cascade):
+
+ registry = util.preloaded.orm_decl_api.registry
+
+ orig = set(registries)
+
+ for reg in registry._recurse_with_dependencies(registries):
+ has_skip = False
+
+ for mapper in reg._mappers_to_configure():
+ 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 - "
+ "can't proceed with initialization of other "
+ "mappers. Triggering mapper: '%s'. "
+ "Original exception was: %s"
+ % (mapper, mapper._configure_failed)
+ )
+ e._configure_failed = mapper._configure_failed
+ raise e
+
+ if not mapper.configured:
+ try:
+ mapper._post_configure_properties()
+ mapper._expire_memoizations()
+ mapper.dispatch.mapper_configured(mapper, mapper.class_)
+ except Exception:
+ exc = sys.exc_info()[1]
+ if not hasattr(exc, "_configure_failed"):
+ mapper._configure_failed = exc
+ raise
+ if not has_skip:
+ reg._new_mappers = False
+
+ if not cascade and reg._dependencies.difference(orig):
+ raise sa_exc.InvalidRequestError(
+ "configure was called with cascade=False but "
+ "additional registries remain"
+ )
+
+
+@util.preload_module("sqlalchemy.orm.decl_api")
+def _dispose_registries(registries, cascade):
+
+ registry = util.preloaded.orm_decl_api.registry
+
+ orig = set(registries)
+
+ for reg in registry._recurse_with_dependents(registries):
+ if not cascade and reg._dependents.difference(orig):
+ raise sa_exc.InvalidRequestError(
+ "Registry has dependent registries that are not disposed; "
+ "pass cascade=True to clear these also"
+ )
+
+ while reg._managers:
+ manager, _ = reg._managers.popitem()
+ reg._dispose_manager_and_mapper(manager)
+
+ reg._non_primary_mappers.clear()
+ reg._dependents.clear()
+ for dep in reg._dependencies:
+ dep._dependents.discard(reg)
+ reg._dependencies.clear()
+ # this wasn't done in the 1.3 clear_mappers() and in fact it
+ # was a bug, as it could cause configure_mappers() to invoke
+ # the "before_configured" event even though mappers had all been
+ # disposed.
+ reg._new_mappers = False
+
+
def reconstructor(fn):
"""Decorate a method as the 'reconstructor' hook.
def _event_on_load(state, ctx):
- instrumenting_mapper = state.manager.info[_INSTRUMENTOR]
+ instrumenting_mapper = state.manager.mapper
+
if instrumenting_mapper._reconstructor:
instrumenting_mapper._reconstructor(state.obj())
-def _event_on_first_init(manager, cls):
- """Initial mapper compilation trigger.
-
- instrumentation calls this one when InstanceState
- is first generated, and is needed for legacy mutable
- attributes to work.
- """
-
- instrumenting_mapper = manager.info.get(_INSTRUMENTOR)
- if instrumenting_mapper:
- if Mapper._new_mappers:
- configure_mappers()
-
-
def _event_on_init(state, args, kwargs):
"""Run init_instance hooks.
"""
- instrumenting_mapper = state.manager.info.get(_INSTRUMENTOR)
+ instrumenting_mapper = state.manager.mapper
if instrumenting_mapper:
- if Mapper._new_mappers:
- configure_mappers()
+ instrumenting_mapper._check_configure()
if instrumenting_mapper._set_polymorphic_identity:
instrumenting_mapper._set_polymorphic_identity(state)
return _orm_annotate(self.__negated_contains_or_equals(other))
@util.memoized_property
- @util.preload_module("sqlalchemy.orm.mapper")
def property(self):
- mapperlib = util.preloaded.orm_mapper
- if mapperlib.Mapper._new_mappers:
- mapperlib.Mapper._configure_all()
+ self.prop.parent._check_configure()
return self.prop
def _with_parent(self, instance, alias_secondary=True, from_entity=None):
return self.entity.mapper
def do_init(self):
-
self._check_conflicts()
self._process_dependent_arguments()
+ self._setup_registry_dependencies()
self._setup_join_conditions()
self._check_cascade_settings(self._cascade)
self._post_init()
super(RelationshipProperty, self).do_init()
self._lazy_strategy = self._get_strategy((("lazy", "select"),))
+ def _setup_registry_dependencies(self):
+ self.parent.mapper.registry._set_depends_on(
+ self.entity.mapper.registry
+ )
+
def _process_dependent_arguments(self):
"""Convert incoming configuration arguments to their
proper form.
_track_overlapping_sync_targets = weakref.WeakKeyDictionary()
- @util.preload_module("sqlalchemy.orm.mapper")
def _warn_for_conflicting_sync_targets(self):
- mapperlib = util.preloaded.orm_mapper
if not self.support_sync:
return
for pr, fr_ in prop_to_from.items():
if (
- pr.mapper in mapperlib._mapper_registry
+ not pr.mapper._dispose_called
and pr not in self.prop._reverse_property
and pr.key not in self.prop._overlaps
and self.prop.key not in pr._overlaps
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import subqueryload
-from sqlalchemy.orm.mapper import _mapper_registry
from sqlalchemy.orm.session import _sessions
from sqlalchemy.processors import to_decimal_processor_factory
from sqlalchemy.processors import to_unicode_processor_factory
def assert_no_mappers():
clear_mappers()
gc_collect()
- assert len(_mapper_registry) == 0
class EnsureZeroed(fixtures.ORMTest):
def setup_test(self):
_sessions.clear()
- _mapper_registry.clear()
+ clear_mappers()
# enable query caching, however make the cache small so that
# the tests don't take too long. issues w/ caching include making
Employee,
)
- configure_mappers()
+ Base.registry.configure()
# no subclasses yet.
assert_raises_message(
Employee,
)
- configure_mappers()
+ Base.registry.configure()
self.assert_compile(
Session().query(Employee),
def test_recompile_on_othermapper(self):
"""declarative version of the same test in mappers.py"""
- from sqlalchemy.orm import mapperlib
-
class User(Base):
__tablename__ = "users"
"User", primaryjoin=user_id == User.id, backref="addresses"
)
- assert mapperlib.Mapper._new_mappers is True
+ assert User.__mapper__.registry._new_mappers is True
u = User() # noqa
assert User.addresses
- assert mapperlib.Mapper._new_mappers is False
+ assert User.__mapper__.registry._new_mappers is False
def test_string_dependency_resolution(self):
class User(Base, fixtures.ComparableEntity):
from sqlalchemy.orm import instrumentation
from sqlalchemy.orm import Mapper
from sqlalchemy.orm import mapper
+from sqlalchemy.orm import mapperlib
from sqlalchemy.orm import query
from sqlalchemy.orm import relationship
from sqlalchemy.orm import selectinload
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import subqueryload
-from sqlalchemy.orm.mapper import _mapper_registry
from sqlalchemy.testing import assert_raises
from sqlalchemy.testing import assert_raises_message
from sqlalchemy.testing import AssertsCompiledSQL
canary.mock_calls,
)
- def test_before_mapper_configured_event(self):
+ @testing.combinations((True,), (False,), argnames="create_dependency")
+ @testing.combinations((True,), (False,), argnames="configure_at_once")
+ def test_before_mapper_configured_event(
+ self, create_dependency, configure_at_once
+ ):
"""Test [ticket:4397].
This event is intended to allow a specific mapper to be skipped during
"""
User, users = self.classes.User, self.tables.users
- mapper(User, users)
+ ump = mapper(User, users)
AnotherBase = declarative_base()
__mapper_args__ = dict(
polymorphic_on="species", polymorphic_identity="Animal"
)
+ if create_dependency:
+ user_id = Column("user_id", ForeignKey(users.c.id))
- # Register the first classes and create their Mappers:
- configure_mappers()
+ if not configure_at_once:
+ # Register the first classes and create their Mappers:
+ configure_mappers()
+
+ unconfigured = list(mapperlib._unconfigured_mappers())
+ eq_(0, len(unconfigured))
- unconfigured = [m for m in _mapper_registry if not m.configured]
- eq_(0, len(unconfigured))
+ if create_dependency:
+ ump.add_property("animal", relationship(Animal))
# Declare a subclass, table and mapper, which refers to one that has
# not been loaded yet (Employer), and therefore cannot be configured:
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))
+ unconfigured = list(mapperlib._unconfigured_mappers())
+
+ if configure_at_once:
+ eq_(3, len(unconfigured))
+ else:
+ 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
s = fixture_session()
s.query(User)
- assert_raises(sa.exc.InvalidRequestError, probe)
+ if create_dependency:
+ assert_raises(sa.exc.InvalidRequestError, probe)
+ else:
+ probe()
# If we disable configuring mappers while querying, then it succeeds:
@event.listens_for(
from sqlalchemy.orm import attributes
from sqlalchemy.orm import backref
from sqlalchemy.orm import class_mapper
+from sqlalchemy.orm import clear_mappers
from sqlalchemy.orm import column_property
from sqlalchemy.orm import composite
from sqlalchemy.orm import configure_mappers
from sqlalchemy.testing import assert_raises_message
from sqlalchemy.testing import AssertsCompiledSQL
from sqlalchemy.testing import eq_
+from sqlalchemy.testing import expect_raises_message
from sqlalchemy.testing import fixtures
from sqlalchemy.testing import is_
+from sqlalchemy.testing import is_false
+from sqlalchemy.testing import is_true
from sqlalchemy.testing import ne_
from sqlalchemy.testing.fixtures import ComparableMixin
from sqlalchemy.testing.fixtures import fixture_session
self.classes.User,
)
- self.mapper(User, users)
+ mp = self.mapper(User, users)
sa.orm.configure_mappers()
- assert sa.orm.mapperlib.Mapper._new_mappers is False
+ assert mp.registry._new_mappers is False
m = self.mapper(
Address,
)
assert m.configured is False
- assert sa.orm.mapperlib.Mapper._new_mappers is True
+ assert m.registry._new_mappers is True
User()
assert User.addresses
- assert sa.orm.mapperlib.Mapper._new_mappers is False
+ assert m.registry._new_mappers is False
def test_configure_on_session(self):
User, users = self.classes.User, self.tables.users
self.mapper(Address, addresses)
configure_mappers()
+ @testing.combinations((True,), (False,))
+ def test_registry_configure(self, cascade):
+ User, users = self.classes.User, self.tables.users
+
+ reg1 = registry()
+ ump = reg1.map_imperatively(User, users)
+
+ reg2 = registry()
+ AnotherBase = reg2.generate_base()
+
+ class Animal(AnotherBase):
+ __tablename__ = "animal"
+ species = Column(String(30), primary_key=True)
+ __mapper_args__ = dict(
+ polymorphic_on="species", polymorphic_identity="Animal"
+ )
+ user_id = Column("user_id", ForeignKey(users.c.id))
+
+ ump.add_property("animal", relationship(Animal))
+
+ if cascade:
+ reg1.configure(cascade=True)
+ else:
+ with expect_raises_message(
+ sa.exc.InvalidRequestError,
+ "configure was called with cascade=False",
+ ):
+ reg1.configure()
+
def test_reconstructor(self):
users = self.tables.users
"foobar(users_1.id) = foobar(:foobar_1)",
dialect=default.DefaultDialect(),
)
+
+
+class RegistryConfigDisposeTest(fixtures.TestBase):
+ """test the cascading behavior of registry configure / dispose."""
+
+ @testing.fixture
+ def threeway_fixture(self):
+ reg1 = registry()
+ reg2 = registry()
+ reg3 = registry()
+
+ ab = bc = True
+
+ @reg1.mapped
+ class A(object):
+ __tablename__ = "a"
+ id = Column(Integer, primary_key=True)
+
+ @reg2.mapped
+ class B(object):
+ __tablename__ = "b"
+ id = Column(Integer, primary_key=True)
+ a_id = Column(ForeignKey(A.id))
+
+ @reg3.mapped
+ class C(object):
+ __tablename__ = "c"
+ id = Column(Integer, primary_key=True)
+ b_id = Column(ForeignKey(B.id))
+
+ if ab:
+ A.__mapper__.add_property("b", relationship(B))
+
+ if bc:
+ B.__mapper__.add_property("c", relationship(C))
+
+ yield reg1, reg2, reg3
+
+ clear_mappers()
+
+ @testing.fixture
+ def threeway_configured_fixture(self, threeway_fixture):
+ reg1, reg2, reg3 = threeway_fixture
+ configure_mappers()
+
+ return reg1, reg2, reg3
+
+ @testing.combinations((True,), (False,), argnames="cascade")
+ def test_configure_cascade_on_dependencies(
+ self, threeway_fixture, cascade
+ ):
+ reg1, reg2, reg3 = threeway_fixture
+ A, B, C = (
+ reg1._class_registry["A"],
+ reg2._class_registry["B"],
+ reg3._class_registry["C"],
+ )
+
+ is_(reg3._new_mappers, True)
+ is_(reg2._new_mappers, True)
+ is_(reg1._new_mappers, True)
+
+ if cascade:
+ reg1.configure(cascade=True)
+
+ is_(reg3._new_mappers, False)
+ is_(reg2._new_mappers, False)
+ is_(reg1._new_mappers, False)
+
+ is_true(C.__mapper__.configured)
+ is_true(B.__mapper__.configured)
+ is_true(A.__mapper__.configured)
+ else:
+ with testing.expect_raises_message(
+ sa.exc.InvalidRequestError,
+ "configure was called with cascade=False but additional ",
+ ):
+ reg1.configure()
+
+ @testing.combinations((True,), (False,), argnames="cascade")
+ def test_configure_cascade_not_on_dependents(
+ self, threeway_fixture, cascade
+ ):
+ reg1, reg2, reg3 = threeway_fixture
+ A, B, C = (
+ reg1._class_registry["A"],
+ reg2._class_registry["B"],
+ reg3._class_registry["C"],
+ )
+
+ is_(reg3._new_mappers, True)
+ is_(reg2._new_mappers, True)
+ is_(reg1._new_mappers, True)
+
+ reg3.configure(cascade=cascade)
+
+ is_(reg3._new_mappers, False)
+ is_(reg2._new_mappers, True)
+ is_(reg1._new_mappers, True)
+
+ is_true(C.__mapper__.configured)
+ is_false(B.__mapper__.configured)
+ is_false(A.__mapper__.configured)
+
+ @testing.combinations((True,), (False,), argnames="cascade")
+ def test_dispose_cascade_not_on_dependencies(
+ self, threeway_configured_fixture, cascade
+ ):
+ reg1, reg2, reg3 = threeway_configured_fixture
+ A, B, C = (
+ reg1._class_registry["A"],
+ reg2._class_registry["B"],
+ reg3._class_registry["C"],
+ )
+ am, bm, cm = A.__mapper__, B.__mapper__, C.__mapper__
+
+ reg1.dispose(cascade=cascade)
+
+ eq_(reg3.mappers, {cm})
+ eq_(reg2.mappers, {bm})
+ eq_(reg1.mappers, set())
+
+ is_false(cm._dispose_called)
+ is_false(bm._dispose_called)
+ is_true(am._dispose_called)
+
+ @testing.combinations((True,), (False,), argnames="cascade")
+ def test_clear_cascade_not_on_dependents(
+ self, threeway_configured_fixture, cascade
+ ):
+ reg1, reg2, reg3 = threeway_configured_fixture
+ A, B, C = (
+ reg1._class_registry["A"],
+ reg2._class_registry["B"],
+ reg3._class_registry["C"],
+ )
+ am, bm, cm = A.__mapper__, B.__mapper__, C.__mapper__
+
+ if cascade:
+ reg3.dispose(cascade=True)
+
+ eq_(reg3.mappers, set())
+ eq_(reg2.mappers, set())
+ eq_(reg1.mappers, set())
+
+ is_true(cm._dispose_called)
+ is_true(bm._dispose_called)
+ is_true(am._dispose_called)
+ else:
+ with testing.expect_raises_message(
+ sa.exc.InvalidRequestError,
+ "Registry has dependent registries that are not disposed; "
+ "pass cascade=True to clear these also",
+ ):
+ reg3.dispose()