]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
reorganize mapper compile/teardown under registry
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 30 Jan 2021 16:55:22 +0000 (11:55 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 1 Feb 2021 21:35:35 +0000 (16:35 -0500)
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.

Fixes: #5897
Change-Id: I14bd96982d6d46e241bd6baa2cf97471d21e7caa

13 files changed:
doc/build/changelog/unreleased_14/5897.rst [new file with mode: 0644]
lib/sqlalchemy/orm/__init__.py
lib/sqlalchemy/orm/base.py
lib/sqlalchemy/orm/decl_api.py
lib/sqlalchemy/orm/instrumentation.py
lib/sqlalchemy/orm/interfaces.py
lib/sqlalchemy/orm/mapper.py
lib/sqlalchemy/orm/relationships.py
test/aaa_profiling/test_memusage.py
test/ext/declarative/test_inheritance.py
test/orm/declarative/test_basic.py
test/orm/test_events.py
test/orm/test_mapper.py

diff --git a/doc/build/changelog/unreleased_14/5897.rst b/doc/build/changelog/unreleased_14/5897.rst
new file mode 100644 (file)
index 0000000..6a9a11c
--- /dev/null
@@ -0,0 +1,13 @@
+.. 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.
index f6b1a2e93f69bc6f1c768003ad1c915079b62c68..ffbe785032f82a8a5ccaaa362ad7c651bee0df3d 100644 (file)
@@ -43,7 +43,6 @@ from .interfaces import ONETOMANY  # noqa
 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
@@ -247,6 +246,10 @@ synonym = public_factory(SynonymProperty, ".orm.synonym")
 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.
@@ -266,14 +269,8 @@ def clear_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
index b805c6f935b209af09aa49346ecaa3ac09c495c3..2932f1bb9e76fc546e461215c1fcaa98d3febe04 100644 (file)
@@ -158,7 +158,6 @@ PASSIVE_ONLY_PERSISTENT = util.symbol(
 
 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")
@@ -412,8 +411,8 @@ def _inspect_mapped_class(class_, configure=False):
     except exc.NO_STATE:
         return None
     else:
-        if configure and mapper._new_mappers:
-            mapper._configure_all()
+        if configure:
+            mapper._check_configure()
         return mapper
 
 
index e6983675bd0189b13651d6fb62bf4437774afa09..e6d41083f17c0abc264079377e7da482122ad24f 100644 (file)
@@ -7,13 +7,16 @@
 """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
@@ -456,12 +459,179 @@ class registry(object):
             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,
@@ -707,6 +877,9 @@ class registry(object):
         return _mapper(self, class_, local_table, kw)
 
 
+mapperlib._legacy_registry = registry()
+
+
 @util.deprecated_params(
     bind=(
         "2.0",
index d2ff72180df4e619c722ab2668ba1d148fa4a598..c970bee22ac1be9ddf830096481dd14975cc5ddb 100644 (file)
@@ -129,7 +129,7 @@ class ClassManager(HasMemoized, dict):
         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:
@@ -278,11 +278,6 @@ class ClassManager(HasMemoized, dict):
 
         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
@@ -359,6 +354,9 @@ class ClassManager(HasMemoized, dict):
             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(
@@ -496,7 +494,7 @@ class _SerializeManager(object):
                 "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.
@@ -538,10 +536,7 @@ class InstrumentationFactory(object):
     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
index bbe39af6038d583341f0cf7fa3fbca1bb565d7e3..994a76bdc855ed577020341cbde7f93fb8e6c6c8 100644 (file)
@@ -221,6 +221,7 @@ class MapperProperty(
         relationships between mappers and perform other post-mapper-creation
         initialization steps.
 
+
         """
         self._configure_started = True
         self.do_init()
index 4ad91dd06333cff63d44e0eba1ee642b74d1ec05..66c0627243c1fa3d7dbddcdfa6e76d7e3afc278e 100644 (file)
@@ -29,7 +29,6 @@ from . import loading
 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
@@ -57,8 +56,21 @@ from ..sql import visitors
 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
 
 
@@ -111,8 +123,8 @@ class Mapper(
 
     """
 
-    _new_mappers = False
     _dispose_called = False
+    _ready_for_configure = False
 
     @util.deprecated_params(
         non_primary=(
@@ -567,7 +579,6 @@ class Mapper(
               techniques.
 
         """
-
         self.class_ = util.assert_arg_type(class_, type, "class_")
         self._sort_key = "%s.%s" % (
             self.class_.__module__,
@@ -620,7 +631,7 @@ class Mapper(
             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
@@ -662,8 +673,6 @@ class Mapper(
         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)
@@ -674,7 +683,7 @@ class Mapper(
             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()
 
@@ -768,7 +777,7 @@ class Mapper(
 
     """
 
-    configured = None
+    configured = False
     """Represent ``True`` if this :class:`_orm.Mapper` has been configured.
 
     This is a *read only* attribute determined during mapper construction.
@@ -1191,8 +1200,9 @@ class Mapper(
                     "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:
@@ -1207,8 +1217,6 @@ class Mapper(
             # 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
@@ -1227,6 +1235,7 @@ class Mapper(
             # and call the class_instrument event
             finalize=True,
         )
+
         if not manager.registry:
             util.warn_deprecated_20(
                 "Calling the mapper() function directly outside of a "
@@ -1234,18 +1243,17 @@ class Mapper(
                 " 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_):
@@ -1270,29 +1278,12 @@ class Mapper(
                             {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)
@@ -1877,6 +1868,10 @@ class Mapper(
                 "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.
@@ -1983,8 +1978,8 @@ class 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]
@@ -2005,8 +2000,8 @@ class Mapper(
     @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):
@@ -2081,8 +2076,8 @@ class Mapper(
 
     @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)
@@ -2098,8 +2093,7 @@ class Mapper(
         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):
@@ -2403,8 +2397,8 @@ class Mapper(
             :attr:`_orm.Mapper.all_orm_descriptors`
 
         """
-        if Mapper._new_mappers:
-            configure_mappers()
+
+        self._check_configure()
         return util.ImmutableProperties(self._props)
 
     @HasMemoized.memoized_attribute
@@ -2559,8 +2553,7 @@ class Mapper(
         )
 
     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_)
@@ -3288,24 +3281,54 @@ class _OptGetColumnsNotAvailable(Exception):
 
 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
@@ -3313,15 +3336,25 @@ def configure_mappers():
       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:
@@ -3332,58 +3365,105 @@ def configure_mappers():
         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.
 
@@ -3460,25 +3540,12 @@ def validates(*names, **kw):
 
 
 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.
 
@@ -3488,10 +3555,9 @@ def _event_on_init(state, args, kwargs):
 
     """
 
-    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)
 
index 41c3a5e53cc5c72156a9e390ac36a9ed5480a525..1a01409148c5646ed2f8cfae06dbb7670344f665 100644 (file)
@@ -1658,11 +1658,8 @@ class RelationshipProperty(StrategizedProperty):
                 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):
@@ -2130,9 +2127,9 @@ class RelationshipProperty(StrategizedProperty):
         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()
@@ -2141,6 +2138,11 @@ class RelationshipProperty(StrategizedProperty):
         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.
@@ -3391,9 +3393,7 @@ class JoinCondition(object):
 
     _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
 
@@ -3424,7 +3424,7 @@ class JoinCondition(object):
 
                 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
index a41a8b9f11c543d21dbccc48b35412c47eedf5e7..dd709965bac904a28a1b6aeffa0c75016b1bfea8 100644 (file)
@@ -27,7 +27,6 @@ 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.orm.session import _sessions
 from sqlalchemy.processors import to_decimal_processor_factory
 from sqlalchemy.processors import to_unicode_processor_factory
@@ -237,13 +236,12 @@ def profile_memory(
 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
index e25e7cfc292faef2169ee63a506bb4d3bdd2309e..496b5579d95292b13853b36417b0aa1d1ddd58ca 100644 (file)
@@ -227,7 +227,7 @@ class ConcreteInhTest(
             Employee,
         )
 
-        configure_mappers()
+        Base.registry.configure()
 
         # no subclasses yet.
         assert_raises_message(
@@ -257,7 +257,7 @@ class ConcreteInhTest(
             Employee,
         )
 
-        configure_mappers()
+        Base.registry.configure()
 
         self.assert_compile(
             Session().query(Employee),
index 4d9162105b81ecc821aae8166848b91ac698e64d..c779d214c873593af440a262b897124c4167b1e3 100644 (file)
@@ -547,8 +547,6 @@ class DeclarativeTest(DeclarativeTestBase):
     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"
 
@@ -565,10 +563,10 @@ class DeclarativeTest(DeclarativeTestBase):
                 "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):
index d9caa221d61e008cbf41ad7dc73c8225d3bc15ca..6ba94096d13d432600668ac1c5df398f6c7e401f 100644 (file)
@@ -19,13 +19,13 @@ from sqlalchemy.orm import EXT_SKIP
 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
@@ -1073,7 +1073,11 @@ class MapperEventsTest(_RemoveListeners, _fixtures.FixtureTest):
             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
@@ -1088,7 +1092,7 @@ class MapperEventsTest(_RemoveListeners, _fixtures.FixtureTest):
         """
 
         User, users = self.classes.User, self.tables.users
-        mapper(User, users)
+        ump = mapper(User, users)
 
         AnotherBase = declarative_base()
 
@@ -1098,12 +1102,18 @@ class MapperEventsTest(_RemoveListeners, _fixtures.FixtureTest):
             __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:
@@ -1111,8 +1121,12 @@ class MapperEventsTest(_RemoveListeners, _fixtures.FixtureTest):
             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
@@ -1121,7 +1135,10 @@ class MapperEventsTest(_RemoveListeners, _fixtures.FixtureTest):
             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(
index d182fd2c1748c69003d670b411a670f4082129d2..ba4cf26373eb0f15ab715f51a44400ad7a770d2b 100644 (file)
@@ -15,6 +15,7 @@ from sqlalchemy.orm import aliased
 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
@@ -31,8 +32,11 @@ from sqlalchemy.testing import assert_raises
 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
@@ -304,9 +308,9 @@ class MapperTest(_fixtures.FixtureTest, AssertsCompiledSQL):
             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,
@@ -315,10 +319,10 @@ class MapperTest(_fixtures.FixtureTest, AssertsCompiledSQL):
         )
 
         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
@@ -1810,6 +1814,35 @@ class MapperTest(_fixtures.FixtureTest, AssertsCompiledSQL):
         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
 
@@ -2810,3 +2843,158 @@ class ComparatorFactoryTest(_fixtures.FixtureTest, AssertsCompiledSQL):
             "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()