Merge "reorganize mapper compile/teardown under registry"

This commit is contained in:
mike bayer
2021-02-01 23:13:42 +00:00
committed by Gerrit Code Review
13 changed files with 623 additions and 178 deletions
+13
View File
@@ -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.
+6 -9
View 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
+2 -3
View 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
+173
View 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",
+5 -10
View 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
+1
View 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()
+191 -125
View 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.
have been constructed thus far across all :class:`_orm.registry`
collections.
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.
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.
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.
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`.
The :func:`.configure_mappers` function provides several event hooks
that can be used to augment its functionality. These methods include:
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)
+8 -8
View 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
+1 -3
View 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
+2 -2
View 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),
+2 -4
View 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):
+27 -10
View 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 = [m for m in _mapper_registry if not m.configured]
eq_(0, len(unconfigured))
unconfigured = list(mapperlib._unconfigured_mappers())
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(
+192 -4
View 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()