summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/orm
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2023-02-02 14:38:37 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2023-02-06 11:08:50 -0500
commit3c6acaba017ef0a0b667864199f103f5eb6f79f9 (patch)
treec8d1ea054d5db18e0d56e2741aa9c75b651af959 /lib/sqlalchemy/orm
parent2459619e751f39a796bb1dd9fe75947dd0961fee (diff)
downloadsqlalchemy-3c6acaba017ef0a0b667864199f103f5eb6f79f9.tar.gz
port history meta to 2.0
first change: Reworked the :ref:`examples_versioned_history` to work with version 2.0, while at the same time improving the overall working of this example to use newer APIs, including a newly added hook :meth:`_orm.MapperEvents.after_mapper_constructed`. second change: Added new event hook :meth:`_orm.MapperEvents.after_mapper_constructed`, which supplies an event hook to take place right as the :class:`_orm.Mapper` object has been fully constructed, but before the :meth:`_orm.registry.configure` call has been called. This allows code that can create additional mappings and table structures based on the initial configuration of a :class:`_orm.Mapper`, which also integrates within Declarative configuration. Previously, when using Declarative, where the :class:`_orm.Mapper` object is created within the class creation process, there was no documented means of running code at this point. The change is to immediately benefit custom mapping schemes such as that of the :ref:`examples_versioned_history` example, which generate additional mappers and tables in response to the creation of mapped classes. third change: The infrequently used :attr:`_orm.Mapper.iterate_properties` attribute and :meth:`_orm.Mapper.get_property` method, which are primarily used internally, no longer implicitly invoke the :meth:`_orm.registry.configure` process. Public access to these methods is extremely rare and the only benefit to having :meth:`_orm.registry.configure` would have been allowing "backref" properties be present in these collections. In order to support the new :meth:`_orm.MapperEvents.after_mapper_constructed` event, iteration and access to the internal :class:`_orm.MapperProperty` objects is now possible without triggering an implicit configure of the mapper itself. The more-public facing route to iteration of all mapper attributes, the :attr:`_orm.Mapper.attrs` collection and similar, will still implicitly invoke the :meth:`_orm.registry.configure` step thus making backref attributes available. In all cases, the :meth:`_orm.registry.configure` is always available to be called directly. fourth change: Fixed obscure ORM inheritance issue caused by :ticket:`8705` where some scenarios of inheriting mappers that indicated groups of columns from the local table and the inheriting table together under a :func:`_orm.column_property` would nonetheless warn that properties of the same name were being combined implicitly. Fixes: #9220 Fixes: #9232 Change-Id: Id335b8e8071c8ea509c057c389df9dcd2059437d
Diffstat (limited to 'lib/sqlalchemy/orm')
-rw-r--r--lib/sqlalchemy/orm/events.py46
-rw-r--r--lib/sqlalchemy/orm/mapper.py45
2 files changed, 71 insertions, 20 deletions
diff --git a/lib/sqlalchemy/orm/events.py b/lib/sqlalchemy/orm/events.py
index 5635c76e2..5e8e9c0d9 100644
--- a/lib/sqlalchemy/orm/events.py
+++ b/lib/sqlalchemy/orm/events.py
@@ -911,7 +911,11 @@ class MapperEvents(event.Events[mapperlib.Mapper[Any]]):
before instrumentation is applied to the mapped class.
This event is the earliest phase of mapper construction.
- Most attributes of the mapper are not yet initialized.
+ Most attributes of the mapper are not yet initialized. To
+ receive an event within initial mapper construction where basic
+ state is available such as the :attr:`_orm.Mapper.attrs` collection,
+ the :meth:`_orm.MapperEvents.after_mapper_constructed` event may
+ be a better choice.
This listener can either be applied to the :class:`_orm.Mapper`
class overall, or to any un-mapped class which serves as a base
@@ -927,6 +931,44 @@ class MapperEvents(event.Events[mapperlib.Mapper[Any]]):
of this event.
:param class\_: the mapped class.
+ .. seealso::
+
+ :meth:`_orm.MapperEvents.after_mapper_constructed`
+
+ """
+
+ def after_mapper_constructed(
+ self, mapper: Mapper[_O], class_: Type[_O]
+ ) -> None:
+ """Receive a class and mapper when the :class:`_orm.Mapper` has been
+ fully constructed.
+
+ This event is called after the initial constructor for
+ :class:`_orm.Mapper` completes. This occurs after the
+ :meth:`_orm.MapperEvents.instrument_class` event and after the
+ :class:`_orm.Mapper` has done an initial pass of its arguments
+ to generate its collection of :class:`_orm.MapperProperty` objects,
+ which are accessible via the :meth:`_orm.Mapper.get_property`
+ method and the :attr:`_orm.Mapper.iterate_properties` attribute.
+
+ This event differs from the
+ :meth:`_orm.MapperEvents.before_mapper_configured` event in that it
+ is invoked within the constructor for :class:`_orm.Mapper`, rather
+ than within the :meth:`_orm.registry.configure` process. Currently,
+ this event is the only one which is appropriate for handlers that
+ wish to create additional mapped classes in response to the
+ construction of this :class:`_orm.Mapper`, which will be part of the
+ same configure step when :meth:`_orm.registry.configure` next runs.
+
+ .. versionadded:: 2.0.2
+
+ .. seealso::
+
+ :ref:`examples_versioning` - an example which illustrates the use
+ of the :meth:`_orm.MapperEvents.before_mapper_configured`
+ event to create new mappers to record change-audit histories on
+ objects.
+
"""
def before_mapper_configured(
@@ -938,7 +980,7 @@ class MapperEvents(event.Events[mapperlib.Mapper[Any]]):
the configure step, by returning the :attr:`.orm.interfaces.EXT_SKIP`
symbol which indicates to the :func:`.configure_mappers` call that this
particular mapper (or hierarchy of mappers, if ``propagate=True`` is
- used) should be skipped in the current configuration run. When one or
+ used) should be skipped in the current configuration run. When one or
more mappers are skipped, the he "new mappers" flag will remain set,
meaning the :func:`.configure_mappers` function will continue to be
called when mappers are used, to continue to try to configure all
diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py
index bb7e470ff..c962a682a 100644
--- a/lib/sqlalchemy/orm/mapper.py
+++ b/lib/sqlalchemy/orm/mapper.py
@@ -861,6 +861,8 @@ class Mapper(
self._log("constructed")
self._expire_memoizations()
+ self.dispatch.after_mapper_constructed(self, self.class_)
+
def _prefer_eager_defaults(self, dialect, table):
if self.eager_defaults == "auto":
if not table.implicit_returning:
@@ -1686,7 +1688,6 @@ class Mapper(
# that's given. For other properties, set them up in _props now.
if self._init_properties:
for key, prop_arg in self._init_properties.items():
-
if not isinstance(prop_arg, MapperProperty):
possible_col_prop = self._make_prop_from_column(
key, prop_arg
@@ -1698,17 +1699,22 @@ class Mapper(
# Column that is local to the local Table, don't set it up
# in ._props yet, integrate it into the order given within
# the Table.
+
+ _map_as_property_now = True
if isinstance(possible_col_prop, properties.ColumnProperty):
- given_col = possible_col_prop.columns[0]
- if self.local_table.c.contains_column(given_col):
- explicit_col_props_by_key[key] = possible_col_prop
- explicit_col_props_by_column[given_col] = (
- key,
- possible_col_prop,
- )
- continue
+ for given_col in possible_col_prop.columns:
+ if self.local_table.c.contains_column(given_col):
+ _map_as_property_now = False
+ explicit_col_props_by_key[key] = possible_col_prop
+ explicit_col_props_by_column[given_col] = (
+ key,
+ possible_col_prop,
+ )
- self._configure_property(key, possible_col_prop, init=False)
+ if _map_as_property_now:
+ self._configure_property(
+ key, possible_col_prop, init=False
+ )
# step 2: pull properties from the inherited mapper. reconcile
# columns with those which are explicit above. for properties that
@@ -1728,10 +1734,12 @@ class Mapper(
incoming_prop=incoming_prop,
)
explicit_col_props_by_key[key] = new_prop
- explicit_col_props_by_column[incoming_prop.columns[0]] = (
- key,
- new_prop,
- )
+
+ for inc_col in incoming_prop.columns:
+ explicit_col_props_by_column[inc_col] = (
+ key,
+ new_prop,
+ )
elif key not in self._props:
self._adapt_inherited_property(key, inherited_prop, False)
@@ -1742,7 +1750,6 @@ class Mapper(
# reconciliation against inherited columns occurs here also.
for column in self.persist_selectable.columns:
-
if column in explicit_col_props_by_column:
# column was explicitly passed to properties; configure
# it now in the order in which it corresponds to the
@@ -2428,7 +2435,7 @@ class Mapper(
return key in self._props
def get_property(
- self, key: str, _configure_mappers: bool = True
+ self, key: str, _configure_mappers: bool = False
) -> MapperProperty[Any]:
"""return a MapperProperty associated with the given key."""
@@ -2439,7 +2446,9 @@ class Mapper(
return self._props[key]
except KeyError as err:
raise sa_exc.InvalidRequestError(
- "Mapper '%s' has no property '%s'" % (self, key)
+ f"Mapper '{self}' has no property '{key}'. If this property "
+ "was indicated from other mappers or configure events, ensure "
+ "registry.configure() has been called."
) from err
def get_property_by_column(
@@ -2454,7 +2463,6 @@ class Mapper(
def iterate_properties(self):
"""return an iterator of all MapperProperty objects."""
- self._check_configure()
return iter(self._props.values())
def _mappers_from_spec(
@@ -4080,6 +4088,7 @@ def _do_configure_registries(
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: