From 3544d89d2de0e910afc3648fe1d34c96900b101b Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 2 Sep 2025 11:10:51 -0400 Subject: [PATCH] support omission of standard event listen example The required targets for before_configured() and after_configured() are the Mapper (and soon to include registry things as well in 2.1), update the automatic doc example thing to be able to be skipped when there are special instructions for the target. Also updates the event docs a bit, which were very old and written in more of that "disorganized wall of details" style that was very hard to unlearn Change-Id: I729d08e25b721f4c993aa1856038b27643fc95d0 --- lib/sqlalchemy/event/__init__.py | 1 + lib/sqlalchemy/event/legacy.py | 18 ++++- lib/sqlalchemy/orm/events.py | 129 ++++++++++++++----------------- 3 files changed, 72 insertions(+), 76 deletions(-) diff --git a/lib/sqlalchemy/event/__init__.py b/lib/sqlalchemy/event/__init__.py index 309b7bd33f..4d18309943 100644 --- a/lib/sqlalchemy/event/__init__.py +++ b/lib/sqlalchemy/event/__init__.py @@ -20,6 +20,7 @@ from .base import _DispatchCommon as _DispatchCommon from .base import dispatcher as dispatcher from .base import Events as Events from .legacy import _legacy_signature as _legacy_signature +from .legacy import _omit_standard_example as _omit_standard_example from .registry import _EventKey as _EventKey from .registry import _ListenerFnType as _ListenerFnType from .registry import EventTarget as EventTarget diff --git a/lib/sqlalchemy/event/legacy.py b/lib/sqlalchemy/event/legacy.py index e60fd9a5e1..03037d9bb7 100644 --- a/lib/sqlalchemy/event/legacy.py +++ b/lib/sqlalchemy/event/legacy.py @@ -18,6 +18,7 @@ from typing import List from typing import Optional from typing import Tuple from typing import Type +from typing import TypeVar from .registry import _ET from .registry import _ListenerFnType @@ -29,14 +30,16 @@ if typing.TYPE_CHECKING: from .base import _HasEventsDispatch -_LegacySignatureType = Tuple[str, List[str], Optional[Callable[..., Any]]] +_F = TypeVar("_F", bound=Callable[..., Any]) + +_LegacySignatureType = Tuple[str, List[str], Callable[..., Any]] def _legacy_signature( since: str, argnames: List[str], converter: Optional[Callable[..., Any]] = None, -) -> Callable[[Callable[..., Any]], Callable[..., Any]]: +) -> Callable[[_F], _F]: """legacy sig decorator @@ -48,7 +51,7 @@ def _legacy_signature( """ - def leg(fn: Callable[..., Any]) -> Callable[..., Any]: + def leg(fn: _F) -> _F: if not hasattr(fn, "_legacy_signatures"): fn._legacy_signatures = [] # type: ignore[attr-defined] fn._legacy_signatures.append((since, argnames, converter)) # type: ignore[attr-defined] # noqa: E501 @@ -57,6 +60,11 @@ def _legacy_signature( return leg +def _omit_standard_example(fn: _F) -> _F: + fn._omit_standard_example = True # type: ignore[attr-defined] + return fn + + def _wrap_fn_for_legacy( dispatch_collection: _ClsLevelDispatch[_ET], fn: _ListenerFnType, @@ -222,6 +230,10 @@ def _augment_fn_docs( parent_dispatch_cls: Type[_HasEventsDispatch[_ET]], fn: _ListenerFnType, ) -> str: + if getattr(fn, "_omit_standard_example", False): + assert fn.__doc__ + return fn.__doc__ + header = ( ".. container:: event_signatures\n\n" " Example argument forms::\n" diff --git a/lib/sqlalchemy/orm/events.py b/lib/sqlalchemy/orm/events.py index 53429139d8..b915cdfec8 100644 --- a/lib/sqlalchemy/orm/events.py +++ b/lib/sqlalchemy/orm/events.py @@ -952,42 +952,42 @@ class MapperEvents(event.Events[mapperlib.Mapper[Any]]): """ + @event._omit_standard_example def before_mapper_configured( self, mapper: Mapper[_O], class_: Type[_O] ) -> None: """Called right before a specific mapper is to be configured. - This event is intended to allow a specific mapper to be skipped during - the configure step, by returning the :attr:`.orm.interfaces.EXT_SKIP` - symbol which indicates to the :func:`.configure_mappers` call that this - particular mapper (or hierarchy of mappers, if ``propagate=True`` is - used) should be skipped in the current configuration run. When one or - more mappers are skipped, the "new mappers" flag will remain set, - meaning the :func:`.configure_mappers` function will continue to be - called when mappers are used, to continue to try to configure all - available mappers. - - In comparison to the other configure-level events, - :meth:`.MapperEvents.before_configured`, - :meth:`.MapperEvents.after_configured`, and - :meth:`.MapperEvents.mapper_configured`, the - :meth:`.MapperEvents.before_mapper_configured` event provides for a - meaningful return value when it is registered with the ``retval=True`` - parameter. - - e.g.:: - + The :meth:`.MapperEvents.before_mapper_configured` event is invoked + for each mapper that is encountered when the + :func:`_orm.configure_mappers` function proceeds through the current + list of not-yet-configured mappers. It is similar to the + :meth:`.MapperEvents.mapper_configured` event, except that it's invoked + right before the configuration occurs, rather than afterwards. + + The :meth:`.MapperEvents.before_mapper_configured` event includes + the special capability where it can force the configure step for a + specific mapper to be skipped; to use this feature, establish + the event using the ``retval=True`` parameter and return + the :attr:`.orm.interfaces.EXT_SKIP` symbol to indicate the mapper + should be left unconfigured:: + + from sqlalchemy import event from sqlalchemy.orm import EXT_SKIP + from sqlalchemy.orm import DeclarativeBase - Base = declarative_base() - DontConfigureBase = declarative_base() + class DontConfigureBase(DeclarativeBase): + pass @event.listens_for( DontConfigureBase, "before_mapper_configured", + # support return values for the event retval=True, + # propagate the listener to all subclasses of + # DontConfigureBase propagate=True, ) def dont_configure(mapper, cls): @@ -1032,15 +1032,14 @@ class MapperEvents(event.Events[mapperlib.Mapper[Any]]): event; this event invokes only after all known mappings have been fully configured. - The :meth:`.MapperEvents.mapper_configured` event, unlike + The :meth:`.MapperEvents.mapper_configured` event, unlike the :meth:`.MapperEvents.before_configured` or - :meth:`.MapperEvents.after_configured`, - is called for each mapper/class individually, and the mapper is - passed to the event itself. It also is called exactly once for - a particular mapper. The event is therefore useful for - configurational steps that benefit from being invoked just once - on a specific mapper basis, which don't require that "backref" - configurations are necessarily ready yet. + :meth:`.MapperEvents.after_configured` events, is called for each + mapper/class individually, and the mapper is passed to the event + itself. It also is called exactly once for a particular mapper. The + event is therefore useful for configurational steps that benefit from + being invoked just once on a specific mapper basis, which don't require + that "backref" configurations are necessarily ready yet. :param mapper: the :class:`_orm.Mapper` which is the target of this event. @@ -1057,6 +1056,7 @@ class MapperEvents(event.Events[mapperlib.Mapper[Any]]): """ # TODO: need coverage for this event + @event._omit_standard_example def before_configured(self) -> None: """Called before a series of mappers have been configured. @@ -1068,9 +1068,15 @@ class MapperEvents(event.Events[mapperlib.Mapper[Any]]): new mappers have been made available and new mapper use is detected. + Similar events to this one include + :meth:`.MapperEvents.after_configured`, which is invoked after a series + of mappers has been configured, as well as + :meth:`.MapperEvents.before_mapper_configured` and + :meth:`.MapperEvents.mapper_configured`, which are both invoked on a + per-mapper basis. + This event can **only** be applied to the :class:`_orm.Mapper` class, - and not to individual mappings or mapped classes. It is only invoked - for all mappings as a whole:: + and not to individual mappings or mapped classes:: from sqlalchemy.orm import Mapper @@ -1078,25 +1084,11 @@ class MapperEvents(event.Events[mapperlib.Mapper[Any]]): @event.listens_for(Mapper, "before_configured") def go(): ... - Contrast this event to :meth:`.MapperEvents.after_configured`, - which is invoked after the series of mappers has been configured, - as well as :meth:`.MapperEvents.before_mapper_configured` - and :meth:`.MapperEvents.mapper_configured`, which are both invoked - on a per-mapper basis. - - Theoretically this event is called once per - application, but is actually called any time new mappers - are to be affected by a :func:`_orm.configure_mappers` - call. If new mappings are constructed after existing ones have - already been used, this event will likely be called again. To ensure - that a particular event is only called once and no further, the - ``once=True`` argument (new in 0.9.4) can be applied:: - - from sqlalchemy.orm import mapper - - - @event.listens_for(mapper, "before_configured", once=True) - def go(): ... + Typically, this event is called once per application, but in practice + may be called more than once, any time new mappers are to be affected + by a :func:`_orm.configure_mappers` call. If new mappings are + constructed after existing ones have already been used, this event will + likely be called again. .. seealso:: @@ -1108,6 +1100,7 @@ class MapperEvents(event.Events[mapperlib.Mapper[Any]]): """ + @event._omit_standard_example def after_configured(self) -> None: """Called after a series of mappers have been configured. @@ -1119,17 +1112,15 @@ class MapperEvents(event.Events[mapperlib.Mapper[Any]]): new mappers have been made available and new mapper use is detected. - Contrast this event to the :meth:`.MapperEvents.mapper_configured` - event, which is called on a per-mapper basis while the configuration - operation proceeds; unlike that event, when this event is invoked, - all cross-configurations (e.g. backrefs) will also have been made - available for any mappers that were pending. - Also contrast to :meth:`.MapperEvents.before_configured`, - which is invoked before the series of mappers has been configured. + Similar events to this one include + :meth:`.MapperEvents.before_configured`, which is invoked before a + series of mappers are configured, as well as + :meth:`.MapperEvents.before_mapper_configured` and + :meth:`.MapperEvents.mapper_configured`, which are both invoked on a + per-mapper basis. This event can **only** be applied to the :class:`_orm.Mapper` class, - and not to individual mappings or - mapped classes. It is only invoked for all mappings as a whole:: + and not to individual mappings or mapped classes:: from sqlalchemy.orm import Mapper @@ -1137,19 +1128,11 @@ class MapperEvents(event.Events[mapperlib.Mapper[Any]]): @event.listens_for(Mapper, "after_configured") def go(): ... - Theoretically this event is called once per - application, but is actually called any time new mappers - have been affected by a :func:`_orm.configure_mappers` - call. If new mappings are constructed after existing ones have - already been used, this event will likely be called again. To ensure - that a particular event is only called once and no further, the - ``once=True`` argument (new in 0.9.4) can be applied:: - - from sqlalchemy.orm import mapper - - - @event.listens_for(mapper, "after_configured", once=True) - def go(): ... + Typically, this event is called once per application, but in practice + may be called more than once, any time new mappers are to be affected + by a :func:`_orm.configure_mappers` call. If new mappings are + constructed after existing ones have already been used, this event will + likely be called again. .. seealso:: -- 2.47.3