From c506e01324cac524ee15257d31d9821aa8a23b95 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 21 Jan 2020 18:41:48 -0500 Subject: [PATCH] Reorganize core event modules to avoid import cycles sqlalchemy.sql.naming was causing a full import of engine due to the DDLEvents dependency. Break out pool, DDL and engine events into new modules specific to those packages; resolve some other import cycles in Core also. Change-Id: Ife8d217e58a26ab3605dd80ee70837968f957eaf --- lib/sqlalchemy/__init__.py | 11 +- lib/sqlalchemy/engine/__init__.py | 1 + lib/sqlalchemy/engine/events.py | 789 ++++++++++++++++++ lib/sqlalchemy/events.py | 1290 +---------------------------- lib/sqlalchemy/pool/__init__.py | 1 + lib/sqlalchemy/pool/events.py | 236 ++++++ lib/sqlalchemy/sql/__init__.py | 1 + lib/sqlalchemy/sql/events.py | 285 +++++++ lib/sqlalchemy/sql/naming.py | 2 +- lib/sqlalchemy/sql/traversals.py | 2 +- 10 files changed, 1323 insertions(+), 1295 deletions(-) create mode 100644 lib/sqlalchemy/engine/events.py create mode 100644 lib/sqlalchemy/pool/events.py create mode 100644 lib/sqlalchemy/sql/events.py diff --git a/lib/sqlalchemy/__init__.py b/lib/sqlalchemy/__init__.py index bff833d879..2d67db24cf 100644 --- a/lib/sqlalchemy/__init__.py +++ b/lib/sqlalchemy/__init__.py @@ -6,6 +6,9 @@ # the MIT License: http://www.opensource.org/licenses/mit-license.php from . import util as _util # noqa +from .engine import create_engine # noqa +from .engine import create_mock_engine # noqa +from .engine import engine_from_config # noqa from .inspection import inspect # noqa from .schema import BLANK_SCHEMA # noqa from .schema import CheckConstraint # noqa @@ -118,14 +121,6 @@ from .types import UnicodeText # noqa from .types import VARBINARY # noqa from .types import VARCHAR # noqa -from .engine import create_engine # noqa nosort -from .engine import engine_from_config # noqa nosort -from .engine import create_mock_engine # noqa nosort - -# these are placed last because there are -# cross dependencies between sqlalchemy.sql and -# sqlalchemy.engine that cause import cycles - __version__ = "1.4.0b1" diff --git a/lib/sqlalchemy/engine/__init__.py b/lib/sqlalchemy/engine/__init__.py index 0c5c6882c0..a129069a40 100644 --- a/lib/sqlalchemy/engine/__init__.py +++ b/lib/sqlalchemy/engine/__init__.py @@ -15,6 +15,7 @@ constructor ``create_engine()``. """ +from . import events # noqa from . import util # noqa from .base import Connection # noqa from .base import Engine # noqa diff --git a/lib/sqlalchemy/engine/events.py b/lib/sqlalchemy/engine/events.py new file mode 100644 index 0000000000..638048e6f1 --- /dev/null +++ b/lib/sqlalchemy/engine/events.py @@ -0,0 +1,789 @@ +# sqlalchemy/engine/events.py +# Copyright (C) 2005-2020 the SQLAlchemy authors and contributors +# +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + + +from .base import Engine +from .interfaces import Connectable +from .interfaces import Dialect +from .. import event +from .. import exc +from .. import util + + +class ConnectionEvents(event.Events): + """Available events for :class:`.Connectable`, which includes + :class:`.Connection` and :class:`.Engine`. + + The methods here define the name of an event as well as the names of + members that are passed to listener functions. + + An event listener can be associated with any :class:`.Connectable` + class or instance, such as an :class:`.Engine`, e.g.:: + + from sqlalchemy import event, create_engine + + def before_cursor_execute(conn, cursor, statement, parameters, context, + executemany): + log.info("Received statement: %s", statement) + + engine = create_engine('postgresql://scott:tiger@localhost/test') + event.listen(engine, "before_cursor_execute", before_cursor_execute) + + or with a specific :class:`.Connection`:: + + with engine.begin() as conn: + @event.listens_for(conn, 'before_cursor_execute') + def before_cursor_execute(conn, cursor, statement, parameters, + context, executemany): + log.info("Received statement: %s", statement) + + When the methods are called with a `statement` parameter, such as in + :meth:`.after_cursor_execute`, :meth:`.before_cursor_execute` and + :meth:`.dbapi_error`, the statement is the exact SQL string that was + prepared for transmission to the DBAPI ``cursor`` in the connection's + :class:`.Dialect`. + + The :meth:`.before_execute` and :meth:`.before_cursor_execute` + events can also be established with the ``retval=True`` flag, which + allows modification of the statement and parameters to be sent + to the database. The :meth:`.before_cursor_execute` event is + particularly useful here to add ad-hoc string transformations, such + as comments, to all executions:: + + from sqlalchemy.engine import Engine + from sqlalchemy import event + + @event.listens_for(Engine, "before_cursor_execute", retval=True) + def comment_sql_calls(conn, cursor, statement, parameters, + context, executemany): + statement = statement + " -- some comment" + return statement, parameters + + .. note:: :class:`.ConnectionEvents` can be established on any + combination of :class:`.Engine`, :class:`.Connection`, as well + as instances of each of those classes. Events across all + four scopes will fire off for a given instance of + :class:`.Connection`. However, for performance reasons, the + :class:`.Connection` object determines at instantiation time + whether or not its parent :class:`.Engine` has event listeners + established. Event listeners added to the :class:`.Engine` + class or to an instance of :class:`.Engine` *after* the instantiation + of a dependent :class:`.Connection` instance will usually + *not* be available on that :class:`.Connection` instance. The newly + added listeners will instead take effect for :class:`.Connection` + instances created subsequent to those event listeners being + established on the parent :class:`.Engine` class or instance. + + :param retval=False: Applies to the :meth:`.before_execute` and + :meth:`.before_cursor_execute` events only. When True, the + user-defined event function must have a return value, which + is a tuple of parameters that replace the given statement + and parameters. See those methods for a description of + specific return arguments. + + """ + + _target_class_doc = "SomeEngine" + _dispatch_target = Connectable + + @classmethod + def _listen(cls, event_key, retval=False): + target, identifier, fn = ( + event_key.dispatch_target, + event_key.identifier, + event_key._listen_fn, + ) + + target._has_events = True + + if not retval: + if identifier == "before_execute": + orig_fn = fn + + def wrap_before_execute( + conn, clauseelement, multiparams, params + ): + orig_fn(conn, clauseelement, multiparams, params) + return clauseelement, multiparams, params + + fn = wrap_before_execute + elif identifier == "before_cursor_execute": + orig_fn = fn + + def wrap_before_cursor_execute( + conn, cursor, statement, parameters, context, executemany + ): + orig_fn( + conn, + cursor, + statement, + parameters, + context, + executemany, + ) + return statement, parameters + + fn = wrap_before_cursor_execute + elif retval and identifier not in ( + "before_execute", + "before_cursor_execute", + "handle_error", + ): + raise exc.ArgumentError( + "Only the 'before_execute', " + "'before_cursor_execute' and 'handle_error' engine " + "event listeners accept the 'retval=True' " + "argument." + ) + event_key.with_wrapper(fn).base_listen() + + def before_execute(self, conn, clauseelement, multiparams, params): + """Intercept high level execute() events, receiving uncompiled + SQL constructs and other objects prior to rendering into SQL. + + This event is good for debugging SQL compilation issues as well + as early manipulation of the parameters being sent to the database, + as the parameter lists will be in a consistent format here. + + This event can be optionally established with the ``retval=True`` + flag. The ``clauseelement``, ``multiparams``, and ``params`` + arguments should be returned as a three-tuple in this case:: + + @event.listens_for(Engine, "before_execute", retval=True) + def before_execute(conn, clauseelement, multiparams, params): + # do something with clauseelement, multiparams, params + return clauseelement, multiparams, params + + :param conn: :class:`.Connection` object + :param clauseelement: SQL expression construct, :class:`.Compiled` + instance, or string statement passed to :meth:`.Connection.execute`. + :param multiparams: Multiple parameter sets, a list of dictionaries. + :param params: Single parameter set, a single dictionary. + + .. seealso:: + + :meth:`.before_cursor_execute` + + """ + + def after_execute(self, conn, clauseelement, multiparams, params, result): + """Intercept high level execute() events after execute. + + + :param conn: :class:`.Connection` object + :param clauseelement: SQL expression construct, :class:`.Compiled` + instance, or string statement passed to :meth:`.Connection.execute`. + :param multiparams: Multiple parameter sets, a list of dictionaries. + :param params: Single parameter set, a single dictionary. + :param result: :class:`.ResultProxy` generated by the execution. + + """ + + def before_cursor_execute( + self, conn, cursor, statement, parameters, context, executemany + ): + """Intercept low-level cursor execute() events before execution, + receiving the string SQL statement and DBAPI-specific parameter list to + be invoked against a cursor. + + This event is a good choice for logging as well as late modifications + to the SQL string. It's less ideal for parameter modifications except + for those which are specific to a target backend. + + This event can be optionally established with the ``retval=True`` + flag. The ``statement`` and ``parameters`` arguments should be + returned as a two-tuple in this case:: + + @event.listens_for(Engine, "before_cursor_execute", retval=True) + def before_cursor_execute(conn, cursor, statement, + parameters, context, executemany): + # do something with statement, parameters + return statement, parameters + + See the example at :class:`.ConnectionEvents`. + + :param conn: :class:`.Connection` object + :param cursor: DBAPI cursor object + :param statement: string SQL statement, as to be passed to the DBAPI + :param parameters: Dictionary, tuple, or list of parameters being + passed to the ``execute()`` or ``executemany()`` method of the + DBAPI ``cursor``. In some cases may be ``None``. + :param context: :class:`.ExecutionContext` object in use. May + be ``None``. + :param executemany: boolean, if ``True``, this is an ``executemany()`` + call, if ``False``, this is an ``execute()`` call. + + .. seealso:: + + :meth:`.before_execute` + + :meth:`.after_cursor_execute` + + """ + + def after_cursor_execute( + self, conn, cursor, statement, parameters, context, executemany + ): + """Intercept low-level cursor execute() events after execution. + + :param conn: :class:`.Connection` object + :param cursor: DBAPI cursor object. Will have results pending + if the statement was a SELECT, but these should not be consumed + as they will be needed by the :class:`.ResultProxy`. + :param statement: string SQL statement, as passed to the DBAPI + :param parameters: Dictionary, tuple, or list of parameters being + passed to the ``execute()`` or ``executemany()`` method of the + DBAPI ``cursor``. In some cases may be ``None``. + :param context: :class:`.ExecutionContext` object in use. May + be ``None``. + :param executemany: boolean, if ``True``, this is an ``executemany()`` + call, if ``False``, this is an ``execute()`` call. + + """ + + @util.deprecated( + "0.9", + "The :meth:`.ConnectionEvents.dbapi_error` " + "event is deprecated and will be removed in a future release. " + "Please refer to the :meth:`.ConnectionEvents.handle_error` " + "event.", + ) + def dbapi_error( + self, conn, cursor, statement, parameters, context, exception + ): + """Intercept a raw DBAPI error. + + This event is called with the DBAPI exception instance + received from the DBAPI itself, *before* SQLAlchemy wraps the + exception with it's own exception wrappers, and before any + other operations are performed on the DBAPI cursor; the + existing transaction remains in effect as well as any state + on the cursor. + + The use case here is to inject low-level exception handling + into an :class:`.Engine`, typically for logging and + debugging purposes. + + .. warning:: + + Code should **not** modify + any state or throw any exceptions here as this will + interfere with SQLAlchemy's cleanup and error handling + routines. For exception modification, please refer to the + new :meth:`.ConnectionEvents.handle_error` event. + + Subsequent to this hook, SQLAlchemy may attempt any + number of operations on the connection/cursor, including + closing the cursor, rolling back of the transaction in the + case of connectionless execution, and disposing of the entire + connection pool if a "disconnect" was detected. The + exception is then wrapped in a SQLAlchemy DBAPI exception + wrapper and re-thrown. + + :param conn: :class:`.Connection` object + :param cursor: DBAPI cursor object + :param statement: string SQL statement, as passed to the DBAPI + :param parameters: Dictionary, tuple, or list of parameters being + passed to the ``execute()`` or ``executemany()`` method of the + DBAPI ``cursor``. In some cases may be ``None``. + :param context: :class:`.ExecutionContext` object in use. May + be ``None``. + :param exception: The **unwrapped** exception emitted directly from the + DBAPI. The class here is specific to the DBAPI module in use. + + """ + + def handle_error(self, exception_context): + r"""Intercept all exceptions processed by the :class:`.Connection`. + + This includes all exceptions emitted by the DBAPI as well as + within SQLAlchemy's statement invocation process, including + encoding errors and other statement validation errors. Other areas + in which the event is invoked include transaction begin and end, + result row fetching, cursor creation. + + Note that :meth:`.handle_error` may support new kinds of exceptions + and new calling scenarios at *any time*. Code which uses this + event must expect new calling patterns to be present in minor + releases. + + To support the wide variety of members that correspond to an exception, + as well as to allow extensibility of the event without backwards + incompatibility, the sole argument received is an instance of + :class:`.ExceptionContext`. This object contains data members + representing detail about the exception. + + Use cases supported by this hook include: + + * read-only, low-level exception handling for logging and + debugging purposes + * exception re-writing + * Establishing or disabling whether a connection or the owning + connection pool is invalidated or expired in response to a + specific exception. + + The hook is called while the cursor from the failed operation + (if any) is still open and accessible. Special cleanup operations + can be called on this cursor; SQLAlchemy will attempt to close + this cursor subsequent to this hook being invoked. If the connection + is in "autocommit" mode, the transaction also remains open within + the scope of this hook; the rollback of the per-statement transaction + also occurs after the hook is called. + + For the common case of detecting a "disconnect" situation which + is not currently handled by the SQLAlchemy dialect, the + :attr:`.ExceptionContext.is_disconnect` flag can be set to True which + will cause the exception to be considered as a disconnect situation, + which typically results in the connection pool being invalidated:: + + @event.listens_for(Engine, "handle_error") + def handle_exception(context): + if isinstance(context.original_exception, pyodbc.Error): + for code in ( + '08S01', '01002', '08003', + '08007', '08S02', '08001', 'HYT00', 'HY010'): + + if code in str(context.original_exception): + context.is_disconnect = True + + A handler function has two options for replacing + the SQLAlchemy-constructed exception into one that is user + defined. It can either raise this new exception directly, in + which case all further event listeners are bypassed and the + exception will be raised, after appropriate cleanup as taken + place:: + + @event.listens_for(Engine, "handle_error") + def handle_exception(context): + if isinstance(context.original_exception, + psycopg2.OperationalError) and \ + "failed" in str(context.original_exception): + raise MySpecialException("failed operation") + + .. warning:: Because the :meth:`.ConnectionEvents.handle_error` + event specifically provides for exceptions to be re-thrown as + the ultimate exception raised by the failed statement, + **stack traces will be misleading** if the user-defined event + handler itself fails and throws an unexpected exception; + the stack trace may not illustrate the actual code line that + failed! It is advised to code carefully here and use + logging and/or inline debugging if unexpected exceptions are + occurring. + + Alternatively, a "chained" style of event handling can be + used, by configuring the handler with the ``retval=True`` + modifier and returning the new exception instance from the + function. In this case, event handling will continue onto the + next handler. The "chained" exception is available using + :attr:`.ExceptionContext.chained_exception`:: + + @event.listens_for(Engine, "handle_error", retval=True) + def handle_exception(context): + if context.chained_exception is not None and \ + "special" in context.chained_exception.message: + return MySpecialException("failed", + cause=context.chained_exception) + + Handlers that return ``None`` may be used within the chain; when + a handler returns ``None``, the previous exception instance, + if any, is maintained as the current exception that is passed onto the + next handler. + + When a custom exception is raised or returned, SQLAlchemy raises + this new exception as-is, it is not wrapped by any SQLAlchemy + object. If the exception is not a subclass of + :class:`sqlalchemy.exc.StatementError`, + certain features may not be available; currently this includes + the ORM's feature of adding a detail hint about "autoflush" to + exceptions raised within the autoflush process. + + :param context: an :class:`.ExceptionContext` object. See this + class for details on all available members. + + .. versionadded:: 0.9.7 Added the + :meth:`.ConnectionEvents.handle_error` hook. + + .. versionchanged:: 1.1 The :meth:`.handle_error` event will now + receive all exceptions that inherit from ``BaseException``, + including ``SystemExit`` and ``KeyboardInterrupt``. The setting for + :attr:`.ExceptionContext.is_disconnect` is ``True`` in this case and + the default for + :attr:`.ExceptionContext.invalidate_pool_on_disconnect` is + ``False``. + + .. versionchanged:: 1.0.0 The :meth:`.handle_error` event is now + invoked when an :class:`.Engine` fails during the initial + call to :meth:`.Engine.connect`, as well as when a + :class:`.Connection` object encounters an error during a + reconnect operation. + + .. versionchanged:: 1.0.0 The :meth:`.handle_error` event is + not fired off when a dialect makes use of the + ``skip_user_error_events`` execution option. This is used + by dialects which intend to catch SQLAlchemy-specific exceptions + within specific operations, such as when the MySQL dialect detects + a table not present within the ``has_table()`` dialect method. + Prior to 1.0.0, code which implements :meth:`.handle_error` needs + to ensure that exceptions thrown in these scenarios are re-raised + without modification. + + """ + + def engine_connect(self, conn, branch): + """Intercept the creation of a new :class:`.Connection`. + + This event is called typically as the direct result of calling + the :meth:`.Engine.connect` method. + + It differs from the :meth:`.PoolEvents.connect` method, which + refers to the actual connection to a database at the DBAPI level; + a DBAPI connection may be pooled and reused for many operations. + In contrast, this event refers only to the production of a higher level + :class:`.Connection` wrapper around such a DBAPI connection. + + It also differs from the :meth:`.PoolEvents.checkout` event + in that it is specific to the :class:`.Connection` object, not the + DBAPI connection that :meth:`.PoolEvents.checkout` deals with, although + this DBAPI connection is available here via the + :attr:`.Connection.connection` attribute. But note there can in fact + be multiple :meth:`.PoolEvents.checkout` events within the lifespan + of a single :class:`.Connection` object, if that :class:`.Connection` + is invalidated and re-established. There can also be multiple + :class:`.Connection` objects generated for the same already-checked-out + DBAPI connection, in the case that a "branch" of a :class:`.Connection` + is produced. + + :param conn: :class:`.Connection` object. + :param branch: if True, this is a "branch" of an existing + :class:`.Connection`. A branch is generated within the course + of a statement execution to invoke supplemental statements, most + typically to pre-execute a SELECT of a default value for the purposes + of an INSERT statement. + + .. versionadded:: 0.9.0 + + .. seealso:: + + :ref:`pool_disconnects_pessimistic` - illustrates how to use + :meth:`.ConnectionEvents.engine_connect` + to transparently ensure pooled connections are connected to the + database. + + :meth:`.PoolEvents.checkout` the lower-level pool checkout event + for an individual DBAPI connection + + :meth:`.ConnectionEvents.set_connection_execution_options` - a copy + of a :class:`.Connection` is also made when the + :meth:`.Connection.execution_options` method is called. + + """ + + def set_connection_execution_options(self, conn, opts): + """Intercept when the :meth:`.Connection.execution_options` + method is called. + + This method is called after the new :class:`.Connection` has been + produced, with the newly updated execution options collection, but + before the :class:`.Dialect` has acted upon any of those new options. + + Note that this method is not called when a new :class:`.Connection` + is produced which is inheriting execution options from its parent + :class:`.Engine`; to intercept this condition, use the + :meth:`.ConnectionEvents.engine_connect` event. + + :param conn: The newly copied :class:`.Connection` object + + :param opts: dictionary of options that were passed to the + :meth:`.Connection.execution_options` method. + + .. versionadded:: 0.9.0 + + .. seealso:: + + :meth:`.ConnectionEvents.set_engine_execution_options` - event + which is called when :meth:`.Engine.execution_options` is called. + + + """ + + def set_engine_execution_options(self, engine, opts): + """Intercept when the :meth:`.Engine.execution_options` + method is called. + + The :meth:`.Engine.execution_options` method produces a shallow + copy of the :class:`.Engine` which stores the new options. That new + :class:`.Engine` is passed here. A particular application of this + method is to add a :meth:`.ConnectionEvents.engine_connect` event + handler to the given :class:`.Engine` which will perform some per- + :class:`.Connection` task specific to these execution options. + + :param conn: The newly copied :class:`.Engine` object + + :param opts: dictionary of options that were passed to the + :meth:`.Connection.execution_options` method. + + .. versionadded:: 0.9.0 + + .. seealso:: + + :meth:`.ConnectionEvents.set_connection_execution_options` - event + which is called when :meth:`.Connection.execution_options` is + called. + + """ + + def engine_disposed(self, engine): + """Intercept when the :meth:`.Engine.dispose` method is called. + + The :meth:`.Engine.dispose` method instructs the engine to + "dispose" of it's connection pool (e.g. :class:`.Pool`), and + replaces it with a new one. Disposing of the old pool has the + effect that existing checked-in connections are closed. The new + pool does not establish any new connections until it is first used. + + This event can be used to indicate that resources related to the + :class:`.Engine` should also be cleaned up, keeping in mind that the + :class:`.Engine` can still be used for new requests in which case + it re-acquires connection resources. + + .. versionadded:: 1.0.5 + + """ + + def begin(self, conn): + """Intercept begin() events. + + :param conn: :class:`.Connection` object + + """ + + def rollback(self, conn): + """Intercept rollback() events, as initiated by a + :class:`.Transaction`. + + Note that the :class:`.Pool` also "auto-rolls back" + a DBAPI connection upon checkin, if the ``reset_on_return`` + flag is set to its default value of ``'rollback'``. + To intercept this + rollback, use the :meth:`.PoolEvents.reset` hook. + + :param conn: :class:`.Connection` object + + .. seealso:: + + :meth:`.PoolEvents.reset` + + """ + + def commit(self, conn): + """Intercept commit() events, as initiated by a + :class:`.Transaction`. + + Note that the :class:`.Pool` may also "auto-commit" + a DBAPI connection upon checkin, if the ``reset_on_return`` + flag is set to the value ``'commit'``. To intercept this + commit, use the :meth:`.PoolEvents.reset` hook. + + :param conn: :class:`.Connection` object + """ + + def savepoint(self, conn, name): + """Intercept savepoint() events. + + :param conn: :class:`.Connection` object + :param name: specified name used for the savepoint. + + """ + + def rollback_savepoint(self, conn, name, context): + """Intercept rollback_savepoint() events. + + :param conn: :class:`.Connection` object + :param name: specified name used for the savepoint. + :param context: :class:`.ExecutionContext` in use. May be ``None``. + + """ + + def release_savepoint(self, conn, name, context): + """Intercept release_savepoint() events. + + :param conn: :class:`.Connection` object + :param name: specified name used for the savepoint. + :param context: :class:`.ExecutionContext` in use. May be ``None``. + + """ + + def begin_twophase(self, conn, xid): + """Intercept begin_twophase() events. + + :param conn: :class:`.Connection` object + :param xid: two-phase XID identifier + + """ + + def prepare_twophase(self, conn, xid): + """Intercept prepare_twophase() events. + + :param conn: :class:`.Connection` object + :param xid: two-phase XID identifier + """ + + def rollback_twophase(self, conn, xid, is_prepared): + """Intercept rollback_twophase() events. + + :param conn: :class:`.Connection` object + :param xid: two-phase XID identifier + :param is_prepared: boolean, indicates if + :meth:`.TwoPhaseTransaction.prepare` was called. + + """ + + def commit_twophase(self, conn, xid, is_prepared): + """Intercept commit_twophase() events. + + :param conn: :class:`.Connection` object + :param xid: two-phase XID identifier + :param is_prepared: boolean, indicates if + :meth:`.TwoPhaseTransaction.prepare` was called. + + """ + + +class DialectEvents(event.Events): + """event interface for execution-replacement functions. + + These events allow direct instrumentation and replacement + of key dialect functions which interact with the DBAPI. + + .. note:: + + :class:`.DialectEvents` hooks should be considered **semi-public** + and experimental. + These hooks are not for general use and are only for those situations + where intricate re-statement of DBAPI mechanics must be injected onto + an existing dialect. For general-use statement-interception events, + please use the :class:`.ConnectionEvents` interface. + + .. seealso:: + + :meth:`.ConnectionEvents.before_cursor_execute` + + :meth:`.ConnectionEvents.before_execute` + + :meth:`.ConnectionEvents.after_cursor_execute` + + :meth:`.ConnectionEvents.after_execute` + + + .. versionadded:: 0.9.4 + + """ + + _target_class_doc = "SomeEngine" + _dispatch_target = Dialect + + @classmethod + def _listen(cls, event_key, retval=False): + target = event_key.dispatch_target + + target._has_events = True + event_key.base_listen() + + @classmethod + def _accept_with(cls, target): + if isinstance(target, type): + if issubclass(target, Engine): + return Dialect + elif issubclass(target, Dialect): + return target + elif isinstance(target, Engine): + return target.dialect + else: + return target + + def do_connect(self, dialect, conn_rec, cargs, cparams): + """Receive connection arguments before a connection is made. + + Return a DBAPI connection to halt further events from invoking; + the returned connection will be used. + + Alternatively, the event can manipulate the cargs and/or cparams + collections; cargs will always be a Python list that can be mutated + in-place and cparams a Python dictionary. Return None to + allow control to pass to the next event handler and ultimately + to allow the dialect to connect normally, given the updated + arguments. + + .. versionadded:: 1.0.3 + + """ + + def do_executemany(self, cursor, statement, parameters, context): + """Receive a cursor to have executemany() called. + + Return the value True to halt further events from invoking, + and to indicate that the cursor execution has already taken + place within the event handler. + + """ + + def do_execute_no_params(self, cursor, statement, context): + """Receive a cursor to have execute() with no parameters called. + + Return the value True to halt further events from invoking, + and to indicate that the cursor execution has already taken + place within the event handler. + + """ + + def do_execute(self, cursor, statement, parameters, context): + """Receive a cursor to have execute() called. + + Return the value True to halt further events from invoking, + and to indicate that the cursor execution has already taken + place within the event handler. + + """ + + def do_setinputsizes( + self, inputsizes, cursor, statement, parameters, context + ): + """Receive the setinputsizes dictionary for possible modification. + + This event is emitted in the case where the dialect makes use of the + DBAPI ``cursor.setinputsizes()`` method which passes information about + parameter binding for a particular statement. The given + ``inputsizes`` dictionary will contain :class:`.BindParameter` objects + as keys, linked to DBAPI-specific type objects as values; for + parameters that are not bound, they are added to the dictionary with + ``None`` as the value, which means the parameter will not be included + in the ultimate setinputsizes call. The event may be used to inspect + and/or log the datatypes that are being bound, as well as to modify the + dictionary in place. Parameters can be added, modified, or removed + from this dictionary. Callers will typically want to inspect the + :attr:`.BindParameter.type` attribute of the given bind objects in + order to make decisions about the DBAPI object. + + After the event, the ``inputsizes`` dictionary is converted into + an appropriate datastructure to be passed to ``cursor.setinputsizes``; + either a list for a positional bound parameter execution style, + or a dictionary of string parameter keys to DBAPI type objects for + a named bound parameter execution style. + + Most dialects **do not use** this method at all; the only built-in + dialect which uses this hook is the cx_Oracle dialect. The hook here + is made available so as to allow customization of how datatypes are set + up with the cx_Oracle DBAPI. + + .. versionadded:: 1.2.9 + + .. seealso:: + + :ref:`cx_oracle_setinputsizes` + + """ + pass diff --git a/lib/sqlalchemy/events.py b/lib/sqlalchemy/events.py index 42f030a641..93ef43815b 100644 --- a/lib/sqlalchemy/events.py +++ b/lib/sqlalchemy/events.py @@ -7,1288 +7,8 @@ """Core event interfaces.""" -from . import event -from . import exc -from . import util -from .engine import Connectable -from .engine import Dialect -from .engine import Engine -from .pool import Pool -from .sql.base import SchemaEventTarget - - -class DDLEvents(event.Events): - """ - Define event listeners for schema objects, - that is, :class:`.SchemaItem` and other :class:`.SchemaEventTarget` - subclasses, including :class:`.MetaData`, :class:`.Table`, - :class:`.Column`. - - :class:`.MetaData` and :class:`.Table` support events - specifically regarding when CREATE and DROP - DDL is emitted to the database. - - Attachment events are also provided to customize - behavior whenever a child schema element is associated - with a parent, such as, when a :class:`.Column` is associated - with its :class:`.Table`, when a :class:`.ForeignKeyConstraint` - is associated with a :class:`.Table`, etc. - - Example using the ``after_create`` event:: - - from sqlalchemy import event - from sqlalchemy import Table, Column, Metadata, Integer - - m = MetaData() - some_table = Table('some_table', m, Column('data', Integer)) - - def after_create(target, connection, **kw): - connection.execute("ALTER TABLE %s SET name=foo_%s" % - (target.name, target.name)) - - event.listen(some_table, "after_create", after_create) - - DDL events integrate closely with the - :class:`.DDL` class and the :class:`.DDLElement` hierarchy - of DDL clause constructs, which are themselves appropriate - as listener callables:: - - from sqlalchemy import DDL - event.listen( - some_table, - "after_create", - DDL("ALTER TABLE %(table)s SET name=foo_%(table)s") - ) - - The methods here define the name of an event as well - as the names of members that are passed to listener - functions. - - For all :class:`.DDLEvent` events, the ``propagate=True`` keyword argument - will ensure that a given event handler is propagated to copies of the - object, which are made when using the :meth:`.Table.tometadata` method:: - - from sqlalchemy import DDL - event.listen( - some_table, - "after_create", - DDL("ALTER TABLE %(table)s SET name=foo_%(table)s"), - propagate=True - ) - - new_table = some_table.tometadata(new_metadata) - - The above :class:`.DDL` object will also be associated with the - :class:`.Table` object represented by ``new_table``. - - .. seealso:: - - :ref:`event_toplevel` - - :class:`.DDLElement` - - :class:`.DDL` - - :ref:`schema_ddl_sequences` - - """ - - _target_class_doc = "SomeSchemaClassOrObject" - _dispatch_target = SchemaEventTarget - - def before_create(self, target, connection, **kw): - r"""Called before CREATE statements are emitted. - - :param target: the :class:`.MetaData` or :class:`.Table` - object which is the target of the event. - :param connection: the :class:`.Connection` where the - CREATE statement or statements will be emitted. - :param \**kw: additional keyword arguments relevant - to the event. The contents of this dictionary - may vary across releases, and include the - list of tables being generated for a metadata-level - event, the checkfirst flag, and other - elements used by internal events. - - :func:`.event.listen` also accepts the ``propagate=True`` - modifier for this event; when True, the listener function will - be established for any copies made of the target object, - i.e. those copies that are generated when - :meth:`.Table.tometadata` is used. - - """ - - def after_create(self, target, connection, **kw): - r"""Called after CREATE statements are emitted. - - :param target: the :class:`.MetaData` or :class:`.Table` - object which is the target of the event. - :param connection: the :class:`.Connection` where the - CREATE statement or statements have been emitted. - :param \**kw: additional keyword arguments relevant - to the event. The contents of this dictionary - may vary across releases, and include the - list of tables being generated for a metadata-level - event, the checkfirst flag, and other - elements used by internal events. - - :func:`.event.listen` also accepts the ``propagate=True`` - modifier for this event; when True, the listener function will - be established for any copies made of the target object, - i.e. those copies that are generated when - :meth:`.Table.tometadata` is used. - - """ - - def before_drop(self, target, connection, **kw): - r"""Called before DROP statements are emitted. - - :param target: the :class:`.MetaData` or :class:`.Table` - object which is the target of the event. - :param connection: the :class:`.Connection` where the - DROP statement or statements will be emitted. - :param \**kw: additional keyword arguments relevant - to the event. The contents of this dictionary - may vary across releases, and include the - list of tables being generated for a metadata-level - event, the checkfirst flag, and other - elements used by internal events. - - :func:`.event.listen` also accepts the ``propagate=True`` - modifier for this event; when True, the listener function will - be established for any copies made of the target object, - i.e. those copies that are generated when - :meth:`.Table.tometadata` is used. - - """ - - def after_drop(self, target, connection, **kw): - r"""Called after DROP statements are emitted. - - :param target: the :class:`.MetaData` or :class:`.Table` - object which is the target of the event. - :param connection: the :class:`.Connection` where the - DROP statement or statements have been emitted. - :param \**kw: additional keyword arguments relevant - to the event. The contents of this dictionary - may vary across releases, and include the - list of tables being generated for a metadata-level - event, the checkfirst flag, and other - elements used by internal events. - - :func:`.event.listen` also accepts the ``propagate=True`` - modifier for this event; when True, the listener function will - be established for any copies made of the target object, - i.e. those copies that are generated when - :meth:`.Table.tometadata` is used. - - """ - - def before_parent_attach(self, target, parent): - """Called before a :class:`.SchemaItem` is associated with - a parent :class:`.SchemaItem`. - - :param target: the target object - :param parent: the parent to which the target is being attached. - - :func:`.event.listen` also accepts the ``propagate=True`` - modifier for this event; when True, the listener function will - be established for any copies made of the target object, - i.e. those copies that are generated when - :meth:`.Table.tometadata` is used. - - """ - - def after_parent_attach(self, target, parent): - """Called after a :class:`.SchemaItem` is associated with - a parent :class:`.SchemaItem`. - - :param target: the target object - :param parent: the parent to which the target is being attached. - - :func:`.event.listen` also accepts the ``propagate=True`` - modifier for this event; when True, the listener function will - be established for any copies made of the target object, - i.e. those copies that are generated when - :meth:`.Table.tometadata` is used. - - """ - - def column_reflect(self, inspector, table, column_info): - """Called for each unit of 'column info' retrieved when - a :class:`.Table` is being reflected. - - The dictionary of column information as returned by the - dialect is passed, and can be modified. The dictionary - is that returned in each element of the list returned - by :meth:`.reflection.Inspector.get_columns`: - - * ``name`` - the column's name - - * ``type`` - the type of this column, which should be an instance - of :class:`~sqlalchemy.types.TypeEngine` - - * ``nullable`` - boolean flag if the column is NULL or NOT NULL - - * ``default`` - the column's server default value. This is - normally specified as a plain string SQL expression, however the - event can pass a :class:`.FetchedValue`, :class:`.DefaultClause`, - or :func:`.sql.expression.text` object as well. - - .. versionchanged:: 1.1.6 - - The :meth:`.DDLEvents.column_reflect` event allows a non - string :class:`.FetchedValue`, - :func:`.sql.expression.text`, or derived object to be - specified as the value of ``default`` in the column - dictionary. - - * ``attrs`` - dict containing optional column attributes - - The event is called before any action is taken against - this dictionary, and the contents can be modified. - The :class:`.Column` specific arguments ``info``, ``key``, - and ``quote`` can also be added to the dictionary and - will be passed to the constructor of :class:`.Column`. - - Note that this event is only meaningful if either - associated with the :class:`.Table` class across the - board, e.g.:: - - from sqlalchemy.schema import Table - from sqlalchemy import event - - def listen_for_reflect(inspector, table, column_info): - "receive a column_reflect event" - # ... - - event.listen( - Table, - 'column_reflect', - listen_for_reflect) - - ...or with a specific :class:`.Table` instance using - the ``listeners`` argument:: - - def listen_for_reflect(inspector, table, column_info): - "receive a column_reflect event" - # ... - - t = Table( - 'sometable', - autoload=True, - listeners=[ - ('column_reflect', listen_for_reflect) - ]) - - This because the reflection process initiated by ``autoload=True`` - completes within the scope of the constructor for :class:`.Table`. - - :func:`.event.listen` also accepts the ``propagate=True`` - modifier for this event; when True, the listener function will - be established for any copies made of the target object, - i.e. those copies that are generated when - :meth:`.Table.tometadata` is used. - - """ - - -class PoolEvents(event.Events): - """Available events for :class:`.Pool`. - - The methods here define the name of an event as well - as the names of members that are passed to listener - functions. - - e.g.:: - - from sqlalchemy import event - - def my_on_checkout(dbapi_conn, connection_rec, connection_proxy): - "handle an on checkout event" - - event.listen(Pool, 'checkout', my_on_checkout) - - In addition to accepting the :class:`.Pool` class and - :class:`.Pool` instances, :class:`.PoolEvents` also accepts - :class:`.Engine` objects and the :class:`.Engine` class as - targets, which will be resolved to the ``.pool`` attribute of the - given engine or the :class:`.Pool` class:: - - engine = create_engine("postgresql://scott:tiger@localhost/test") - - # will associate with engine.pool - event.listen(engine, 'checkout', my_on_checkout) - - """ - - _target_class_doc = "SomeEngineOrPool" - _dispatch_target = Pool - - @classmethod - def _accept_with(cls, target): - if isinstance(target, type): - if issubclass(target, Engine): - return Pool - elif issubclass(target, Pool): - return target - elif isinstance(target, Engine): - return target.pool - else: - return target - - def connect(self, dbapi_connection, connection_record): - """Called at the moment a particular DBAPI connection is first - created for a given :class:`.Pool`. - - This event allows one to capture the point directly after which - the DBAPI module-level ``.connect()`` method has been used in order - to produce a new DBAPI connection. - - :param dbapi_connection: a DBAPI connection. - - :param connection_record: the :class:`._ConnectionRecord` managing the - DBAPI connection. - - """ - - def first_connect(self, dbapi_connection, connection_record): - """Called exactly once for the first time a DBAPI connection is - checked out from a particular :class:`.Pool`. - - The rationale for :meth:`.PoolEvents.first_connect` is to determine - information about a particular series of database connections based - on the settings used for all connections. Since a particular - :class:`.Pool` refers to a single "creator" function (which in terms - of a :class:`.Engine` refers to the URL and connection options used), - it is typically valid to make observations about a single connection - that can be safely assumed to be valid about all subsequent - connections, such as the database version, the server and client - encoding settings, collation settings, and many others. - - :param dbapi_connection: a DBAPI connection. - - :param connection_record: the :class:`._ConnectionRecord` managing the - DBAPI connection. - - """ - - def checkout(self, dbapi_connection, connection_record, connection_proxy): - """Called when a connection is retrieved from the Pool. - - :param dbapi_connection: a DBAPI connection. - - :param connection_record: the :class:`._ConnectionRecord` managing the - DBAPI connection. - - :param connection_proxy: the :class:`._ConnectionFairy` object which - will proxy the public interface of the DBAPI connection for the - lifespan of the checkout. - - If you raise a :class:`~sqlalchemy.exc.DisconnectionError`, the current - connection will be disposed and a fresh connection retrieved. - Processing of all checkout listeners will abort and restart - using the new connection. - - .. seealso:: :meth:`.ConnectionEvents.engine_connect` - a similar event - which occurs upon creation of a new :class:`.Connection`. - - """ - - def checkin(self, dbapi_connection, connection_record): - """Called when a connection returns to the pool. - - Note that the connection may be closed, and may be None if the - connection has been invalidated. ``checkin`` will not be called - for detached connections. (They do not return to the pool.) - - :param dbapi_connection: a DBAPI connection. - - :param connection_record: the :class:`._ConnectionRecord` managing the - DBAPI connection. - - """ - - def reset(self, dbapi_connection, connection_record): - """Called before the "reset" action occurs for a pooled connection. - - This event represents - when the ``rollback()`` method is called on the DBAPI connection - before it is returned to the pool. The behavior of "reset" can - be controlled, including disabled, using the ``reset_on_return`` - pool argument. - - - The :meth:`.PoolEvents.reset` event is usually followed by the - :meth:`.PoolEvents.checkin` event is called, except in those - cases where the connection is discarded immediately after reset. - - :param dbapi_connection: a DBAPI connection. - - :param connection_record: the :class:`._ConnectionRecord` managing the - DBAPI connection. - - .. seealso:: - - :meth:`.ConnectionEvents.rollback` - - :meth:`.ConnectionEvents.commit` - - """ - - def invalidate(self, dbapi_connection, connection_record, exception): - """Called when a DBAPI connection is to be "invalidated". - - This event is called any time the :meth:`._ConnectionRecord.invalidate` - method is invoked, either from API usage or via "auto-invalidation", - without the ``soft`` flag. - - The event occurs before a final attempt to call ``.close()`` on the - connection occurs. - - :param dbapi_connection: a DBAPI connection. - - :param connection_record: the :class:`._ConnectionRecord` managing the - DBAPI connection. - - :param exception: the exception object corresponding to the reason - for this invalidation, if any. May be ``None``. - - .. versionadded:: 0.9.2 Added support for connection invalidation - listening. - - .. seealso:: - - :ref:`pool_connection_invalidation` - - """ - - def soft_invalidate(self, dbapi_connection, connection_record, exception): - """Called when a DBAPI connection is to be "soft invalidated". - - This event is called any time the :meth:`._ConnectionRecord.invalidate` - method is invoked with the ``soft`` flag. - - Soft invalidation refers to when the connection record that tracks - this connection will force a reconnect after the current connection - is checked in. It does not actively close the dbapi_connection - at the point at which it is called. - - .. versionadded:: 1.0.3 - - """ - - def close(self, dbapi_connection, connection_record): - """Called when a DBAPI connection is closed. - - The event is emitted before the close occurs. - - The close of a connection can fail; typically this is because - the connection is already closed. If the close operation fails, - the connection is discarded. - - The :meth:`.close` event corresponds to a connection that's still - associated with the pool. To intercept close events for detached - connections use :meth:`.close_detached`. - - .. versionadded:: 1.1 - - """ - - def detach(self, dbapi_connection, connection_record): - """Called when a DBAPI connection is "detached" from a pool. - - This event is emitted after the detach occurs. The connection - is no longer associated with the given connection record. - - .. versionadded:: 1.1 - - """ - - def close_detached(self, dbapi_connection): - """Called when a detached DBAPI connection is closed. - - The event is emitted before the close occurs. - - The close of a connection can fail; typically this is because - the connection is already closed. If the close operation fails, - the connection is discarded. - - .. versionadded:: 1.1 - - """ - - -class ConnectionEvents(event.Events): - """Available events for :class:`.Connectable`, which includes - :class:`.Connection` and :class:`.Engine`. - - The methods here define the name of an event as well as the names of - members that are passed to listener functions. - - An event listener can be associated with any :class:`.Connectable` - class or instance, such as an :class:`.Engine`, e.g.:: - - from sqlalchemy import event, create_engine - - def before_cursor_execute(conn, cursor, statement, parameters, context, - executemany): - log.info("Received statement: %s", statement) - - engine = create_engine('postgresql://scott:tiger@localhost/test') - event.listen(engine, "before_cursor_execute", before_cursor_execute) - - or with a specific :class:`.Connection`:: - - with engine.begin() as conn: - @event.listens_for(conn, 'before_cursor_execute') - def before_cursor_execute(conn, cursor, statement, parameters, - context, executemany): - log.info("Received statement: %s", statement) - - When the methods are called with a `statement` parameter, such as in - :meth:`.after_cursor_execute`, :meth:`.before_cursor_execute` and - :meth:`.dbapi_error`, the statement is the exact SQL string that was - prepared for transmission to the DBAPI ``cursor`` in the connection's - :class:`.Dialect`. - - The :meth:`.before_execute` and :meth:`.before_cursor_execute` - events can also be established with the ``retval=True`` flag, which - allows modification of the statement and parameters to be sent - to the database. The :meth:`.before_cursor_execute` event is - particularly useful here to add ad-hoc string transformations, such - as comments, to all executions:: - - from sqlalchemy.engine import Engine - from sqlalchemy import event - - @event.listens_for(Engine, "before_cursor_execute", retval=True) - def comment_sql_calls(conn, cursor, statement, parameters, - context, executemany): - statement = statement + " -- some comment" - return statement, parameters - - .. note:: :class:`.ConnectionEvents` can be established on any - combination of :class:`.Engine`, :class:`.Connection`, as well - as instances of each of those classes. Events across all - four scopes will fire off for a given instance of - :class:`.Connection`. However, for performance reasons, the - :class:`.Connection` object determines at instantiation time - whether or not its parent :class:`.Engine` has event listeners - established. Event listeners added to the :class:`.Engine` - class or to an instance of :class:`.Engine` *after* the instantiation - of a dependent :class:`.Connection` instance will usually - *not* be available on that :class:`.Connection` instance. The newly - added listeners will instead take effect for :class:`.Connection` - instances created subsequent to those event listeners being - established on the parent :class:`.Engine` class or instance. - - :param retval=False: Applies to the :meth:`.before_execute` and - :meth:`.before_cursor_execute` events only. When True, the - user-defined event function must have a return value, which - is a tuple of parameters that replace the given statement - and parameters. See those methods for a description of - specific return arguments. - - """ - - _target_class_doc = "SomeEngine" - _dispatch_target = Connectable - - @classmethod - def _listen(cls, event_key, retval=False): - target, identifier, fn = ( - event_key.dispatch_target, - event_key.identifier, - event_key._listen_fn, - ) - - target._has_events = True - - if not retval: - if identifier == "before_execute": - orig_fn = fn - - def wrap_before_execute( - conn, clauseelement, multiparams, params - ): - orig_fn(conn, clauseelement, multiparams, params) - return clauseelement, multiparams, params - - fn = wrap_before_execute - elif identifier == "before_cursor_execute": - orig_fn = fn - - def wrap_before_cursor_execute( - conn, cursor, statement, parameters, context, executemany - ): - orig_fn( - conn, - cursor, - statement, - parameters, - context, - executemany, - ) - return statement, parameters - - fn = wrap_before_cursor_execute - elif retval and identifier not in ( - "before_execute", - "before_cursor_execute", - "handle_error", - ): - raise exc.ArgumentError( - "Only the 'before_execute', " - "'before_cursor_execute' and 'handle_error' engine " - "event listeners accept the 'retval=True' " - "argument." - ) - event_key.with_wrapper(fn).base_listen() - - def before_execute(self, conn, clauseelement, multiparams, params): - """Intercept high level execute() events, receiving uncompiled - SQL constructs and other objects prior to rendering into SQL. - - This event is good for debugging SQL compilation issues as well - as early manipulation of the parameters being sent to the database, - as the parameter lists will be in a consistent format here. - - This event can be optionally established with the ``retval=True`` - flag. The ``clauseelement``, ``multiparams``, and ``params`` - arguments should be returned as a three-tuple in this case:: - - @event.listens_for(Engine, "before_execute", retval=True) - def before_execute(conn, clauseelement, multiparams, params): - # do something with clauseelement, multiparams, params - return clauseelement, multiparams, params - - :param conn: :class:`.Connection` object - :param clauseelement: SQL expression construct, :class:`.Compiled` - instance, or string statement passed to :meth:`.Connection.execute`. - :param multiparams: Multiple parameter sets, a list of dictionaries. - :param params: Single parameter set, a single dictionary. - - .. seealso:: - - :meth:`.before_cursor_execute` - - """ - - def after_execute(self, conn, clauseelement, multiparams, params, result): - """Intercept high level execute() events after execute. - - - :param conn: :class:`.Connection` object - :param clauseelement: SQL expression construct, :class:`.Compiled` - instance, or string statement passed to :meth:`.Connection.execute`. - :param multiparams: Multiple parameter sets, a list of dictionaries. - :param params: Single parameter set, a single dictionary. - :param result: :class:`.ResultProxy` generated by the execution. - - """ - - def before_cursor_execute( - self, conn, cursor, statement, parameters, context, executemany - ): - """Intercept low-level cursor execute() events before execution, - receiving the string SQL statement and DBAPI-specific parameter list to - be invoked against a cursor. - - This event is a good choice for logging as well as late modifications - to the SQL string. It's less ideal for parameter modifications except - for those which are specific to a target backend. - - This event can be optionally established with the ``retval=True`` - flag. The ``statement`` and ``parameters`` arguments should be - returned as a two-tuple in this case:: - - @event.listens_for(Engine, "before_cursor_execute", retval=True) - def before_cursor_execute(conn, cursor, statement, - parameters, context, executemany): - # do something with statement, parameters - return statement, parameters - - See the example at :class:`.ConnectionEvents`. - - :param conn: :class:`.Connection` object - :param cursor: DBAPI cursor object - :param statement: string SQL statement, as to be passed to the DBAPI - :param parameters: Dictionary, tuple, or list of parameters being - passed to the ``execute()`` or ``executemany()`` method of the - DBAPI ``cursor``. In some cases may be ``None``. - :param context: :class:`.ExecutionContext` object in use. May - be ``None``. - :param executemany: boolean, if ``True``, this is an ``executemany()`` - call, if ``False``, this is an ``execute()`` call. - - .. seealso:: - - :meth:`.before_execute` - - :meth:`.after_cursor_execute` - - """ - - def after_cursor_execute( - self, conn, cursor, statement, parameters, context, executemany - ): - """Intercept low-level cursor execute() events after execution. - - :param conn: :class:`.Connection` object - :param cursor: DBAPI cursor object. Will have results pending - if the statement was a SELECT, but these should not be consumed - as they will be needed by the :class:`.ResultProxy`. - :param statement: string SQL statement, as passed to the DBAPI - :param parameters: Dictionary, tuple, or list of parameters being - passed to the ``execute()`` or ``executemany()`` method of the - DBAPI ``cursor``. In some cases may be ``None``. - :param context: :class:`.ExecutionContext` object in use. May - be ``None``. - :param executemany: boolean, if ``True``, this is an ``executemany()`` - call, if ``False``, this is an ``execute()`` call. - - """ - - @util.deprecated( - "0.9", - "The :meth:`.ConnectionEvents.dbapi_error` " - "event is deprecated and will be removed in a future release. " - "Please refer to the :meth:`.ConnectionEvents.handle_error` " - "event.", - ) - def dbapi_error( - self, conn, cursor, statement, parameters, context, exception - ): - """Intercept a raw DBAPI error. - - This event is called with the DBAPI exception instance - received from the DBAPI itself, *before* SQLAlchemy wraps the - exception with it's own exception wrappers, and before any - other operations are performed on the DBAPI cursor; the - existing transaction remains in effect as well as any state - on the cursor. - - The use case here is to inject low-level exception handling - into an :class:`.Engine`, typically for logging and - debugging purposes. - - .. warning:: - - Code should **not** modify - any state or throw any exceptions here as this will - interfere with SQLAlchemy's cleanup and error handling - routines. For exception modification, please refer to the - new :meth:`.ConnectionEvents.handle_error` event. - - Subsequent to this hook, SQLAlchemy may attempt any - number of operations on the connection/cursor, including - closing the cursor, rolling back of the transaction in the - case of connectionless execution, and disposing of the entire - connection pool if a "disconnect" was detected. The - exception is then wrapped in a SQLAlchemy DBAPI exception - wrapper and re-thrown. - - :param conn: :class:`.Connection` object - :param cursor: DBAPI cursor object - :param statement: string SQL statement, as passed to the DBAPI - :param parameters: Dictionary, tuple, or list of parameters being - passed to the ``execute()`` or ``executemany()`` method of the - DBAPI ``cursor``. In some cases may be ``None``. - :param context: :class:`.ExecutionContext` object in use. May - be ``None``. - :param exception: The **unwrapped** exception emitted directly from the - DBAPI. The class here is specific to the DBAPI module in use. - - """ - - def handle_error(self, exception_context): - r"""Intercept all exceptions processed by the :class:`.Connection`. - - This includes all exceptions emitted by the DBAPI as well as - within SQLAlchemy's statement invocation process, including - encoding errors and other statement validation errors. Other areas - in which the event is invoked include transaction begin and end, - result row fetching, cursor creation. - - Note that :meth:`.handle_error` may support new kinds of exceptions - and new calling scenarios at *any time*. Code which uses this - event must expect new calling patterns to be present in minor - releases. - - To support the wide variety of members that correspond to an exception, - as well as to allow extensibility of the event without backwards - incompatibility, the sole argument received is an instance of - :class:`.ExceptionContext`. This object contains data members - representing detail about the exception. - - Use cases supported by this hook include: - - * read-only, low-level exception handling for logging and - debugging purposes - * exception re-writing - * Establishing or disabling whether a connection or the owning - connection pool is invalidated or expired in response to a - specific exception. - - The hook is called while the cursor from the failed operation - (if any) is still open and accessible. Special cleanup operations - can be called on this cursor; SQLAlchemy will attempt to close - this cursor subsequent to this hook being invoked. If the connection - is in "autocommit" mode, the transaction also remains open within - the scope of this hook; the rollback of the per-statement transaction - also occurs after the hook is called. - - For the common case of detecting a "disconnect" situation which - is not currently handled by the SQLAlchemy dialect, the - :attr:`.ExceptionContext.is_disconnect` flag can be set to True which - will cause the exception to be considered as a disconnect situation, - which typically results in the connection pool being invalidated:: - - @event.listens_for(Engine, "handle_error") - def handle_exception(context): - if isinstance(context.original_exception, pyodbc.Error): - for code in ( - '08S01', '01002', '08003', - '08007', '08S02', '08001', 'HYT00', 'HY010'): - - if code in str(context.original_exception): - context.is_disconnect = True - - A handler function has two options for replacing - the SQLAlchemy-constructed exception into one that is user - defined. It can either raise this new exception directly, in - which case all further event listeners are bypassed and the - exception will be raised, after appropriate cleanup as taken - place:: - - @event.listens_for(Engine, "handle_error") - def handle_exception(context): - if isinstance(context.original_exception, - psycopg2.OperationalError) and \ - "failed" in str(context.original_exception): - raise MySpecialException("failed operation") - - .. warning:: Because the :meth:`.ConnectionEvents.handle_error` - event specifically provides for exceptions to be re-thrown as - the ultimate exception raised by the failed statement, - **stack traces will be misleading** if the user-defined event - handler itself fails and throws an unexpected exception; - the stack trace may not illustrate the actual code line that - failed! It is advised to code carefully here and use - logging and/or inline debugging if unexpected exceptions are - occurring. - - Alternatively, a "chained" style of event handling can be - used, by configuring the handler with the ``retval=True`` - modifier and returning the new exception instance from the - function. In this case, event handling will continue onto the - next handler. The "chained" exception is available using - :attr:`.ExceptionContext.chained_exception`:: - - @event.listens_for(Engine, "handle_error", retval=True) - def handle_exception(context): - if context.chained_exception is not None and \ - "special" in context.chained_exception.message: - return MySpecialException("failed", - cause=context.chained_exception) - - Handlers that return ``None`` may be used within the chain; when - a handler returns ``None``, the previous exception instance, - if any, is maintained as the current exception that is passed onto the - next handler. - - When a custom exception is raised or returned, SQLAlchemy raises - this new exception as-is, it is not wrapped by any SQLAlchemy - object. If the exception is not a subclass of - :class:`sqlalchemy.exc.StatementError`, - certain features may not be available; currently this includes - the ORM's feature of adding a detail hint about "autoflush" to - exceptions raised within the autoflush process. - - :param context: an :class:`.ExceptionContext` object. See this - class for details on all available members. - - .. versionadded:: 0.9.7 Added the - :meth:`.ConnectionEvents.handle_error` hook. - - .. versionchanged:: 1.1 The :meth:`.handle_error` event will now - receive all exceptions that inherit from ``BaseException``, - including ``SystemExit`` and ``KeyboardInterrupt``. The setting for - :attr:`.ExceptionContext.is_disconnect` is ``True`` in this case and - the default for - :attr:`.ExceptionContext.invalidate_pool_on_disconnect` is - ``False``. - - .. versionchanged:: 1.0.0 The :meth:`.handle_error` event is now - invoked when an :class:`.Engine` fails during the initial - call to :meth:`.Engine.connect`, as well as when a - :class:`.Connection` object encounters an error during a - reconnect operation. - - .. versionchanged:: 1.0.0 The :meth:`.handle_error` event is - not fired off when a dialect makes use of the - ``skip_user_error_events`` execution option. This is used - by dialects which intend to catch SQLAlchemy-specific exceptions - within specific operations, such as when the MySQL dialect detects - a table not present within the ``has_table()`` dialect method. - Prior to 1.0.0, code which implements :meth:`.handle_error` needs - to ensure that exceptions thrown in these scenarios are re-raised - without modification. - - """ - - def engine_connect(self, conn, branch): - """Intercept the creation of a new :class:`.Connection`. - - This event is called typically as the direct result of calling - the :meth:`.Engine.connect` method. - - It differs from the :meth:`.PoolEvents.connect` method, which - refers to the actual connection to a database at the DBAPI level; - a DBAPI connection may be pooled and reused for many operations. - In contrast, this event refers only to the production of a higher level - :class:`.Connection` wrapper around such a DBAPI connection. - - It also differs from the :meth:`.PoolEvents.checkout` event - in that it is specific to the :class:`.Connection` object, not the - DBAPI connection that :meth:`.PoolEvents.checkout` deals with, although - this DBAPI connection is available here via the - :attr:`.Connection.connection` attribute. But note there can in fact - be multiple :meth:`.PoolEvents.checkout` events within the lifespan - of a single :class:`.Connection` object, if that :class:`.Connection` - is invalidated and re-established. There can also be multiple - :class:`.Connection` objects generated for the same already-checked-out - DBAPI connection, in the case that a "branch" of a :class:`.Connection` - is produced. - - :param conn: :class:`.Connection` object. - :param branch: if True, this is a "branch" of an existing - :class:`.Connection`. A branch is generated within the course - of a statement execution to invoke supplemental statements, most - typically to pre-execute a SELECT of a default value for the purposes - of an INSERT statement. - - .. versionadded:: 0.9.0 - - .. seealso:: - - :ref:`pool_disconnects_pessimistic` - illustrates how to use - :meth:`.ConnectionEvents.engine_connect` - to transparently ensure pooled connections are connected to the - database. - - :meth:`.PoolEvents.checkout` the lower-level pool checkout event - for an individual DBAPI connection - - :meth:`.ConnectionEvents.set_connection_execution_options` - a copy - of a :class:`.Connection` is also made when the - :meth:`.Connection.execution_options` method is called. - - """ - - def set_connection_execution_options(self, conn, opts): - """Intercept when the :meth:`.Connection.execution_options` - method is called. - - This method is called after the new :class:`.Connection` has been - produced, with the newly updated execution options collection, but - before the :class:`.Dialect` has acted upon any of those new options. - - Note that this method is not called when a new :class:`.Connection` - is produced which is inheriting execution options from its parent - :class:`.Engine`; to intercept this condition, use the - :meth:`.ConnectionEvents.engine_connect` event. - - :param conn: The newly copied :class:`.Connection` object - - :param opts: dictionary of options that were passed to the - :meth:`.Connection.execution_options` method. - - .. versionadded:: 0.9.0 - - .. seealso:: - - :meth:`.ConnectionEvents.set_engine_execution_options` - event - which is called when :meth:`.Engine.execution_options` is called. - - - """ - - def set_engine_execution_options(self, engine, opts): - """Intercept when the :meth:`.Engine.execution_options` - method is called. - - The :meth:`.Engine.execution_options` method produces a shallow - copy of the :class:`.Engine` which stores the new options. That new - :class:`.Engine` is passed here. A particular application of this - method is to add a :meth:`.ConnectionEvents.engine_connect` event - handler to the given :class:`.Engine` which will perform some per- - :class:`.Connection` task specific to these execution options. - - :param conn: The newly copied :class:`.Engine` object - - :param opts: dictionary of options that were passed to the - :meth:`.Connection.execution_options` method. - - .. versionadded:: 0.9.0 - - .. seealso:: - - :meth:`.ConnectionEvents.set_connection_execution_options` - event - which is called when :meth:`.Connection.execution_options` is - called. - - """ - - def engine_disposed(self, engine): - """Intercept when the :meth:`.Engine.dispose` method is called. - - The :meth:`.Engine.dispose` method instructs the engine to - "dispose" of it's connection pool (e.g. :class:`.Pool`), and - replaces it with a new one. Disposing of the old pool has the - effect that existing checked-in connections are closed. The new - pool does not establish any new connections until it is first used. - - This event can be used to indicate that resources related to the - :class:`.Engine` should also be cleaned up, keeping in mind that the - :class:`.Engine` can still be used for new requests in which case - it re-acquires connection resources. - - .. versionadded:: 1.0.5 - - """ - - def begin(self, conn): - """Intercept begin() events. - - :param conn: :class:`.Connection` object - - """ - - def rollback(self, conn): - """Intercept rollback() events, as initiated by a - :class:`.Transaction`. - - Note that the :class:`.Pool` also "auto-rolls back" - a DBAPI connection upon checkin, if the ``reset_on_return`` - flag is set to its default value of ``'rollback'``. - To intercept this - rollback, use the :meth:`.PoolEvents.reset` hook. - - :param conn: :class:`.Connection` object - - .. seealso:: - - :meth:`.PoolEvents.reset` - - """ - - def commit(self, conn): - """Intercept commit() events, as initiated by a - :class:`.Transaction`. - - Note that the :class:`.Pool` may also "auto-commit" - a DBAPI connection upon checkin, if the ``reset_on_return`` - flag is set to the value ``'commit'``. To intercept this - commit, use the :meth:`.PoolEvents.reset` hook. - - :param conn: :class:`.Connection` object - """ - - def savepoint(self, conn, name): - """Intercept savepoint() events. - - :param conn: :class:`.Connection` object - :param name: specified name used for the savepoint. - - """ - - def rollback_savepoint(self, conn, name, context): - """Intercept rollback_savepoint() events. - - :param conn: :class:`.Connection` object - :param name: specified name used for the savepoint. - :param context: :class:`.ExecutionContext` in use. May be ``None``. - - """ - - def release_savepoint(self, conn, name, context): - """Intercept release_savepoint() events. - - :param conn: :class:`.Connection` object - :param name: specified name used for the savepoint. - :param context: :class:`.ExecutionContext` in use. May be ``None``. - - """ - - def begin_twophase(self, conn, xid): - """Intercept begin_twophase() events. - - :param conn: :class:`.Connection` object - :param xid: two-phase XID identifier - - """ - - def prepare_twophase(self, conn, xid): - """Intercept prepare_twophase() events. - - :param conn: :class:`.Connection` object - :param xid: two-phase XID identifier - """ - - def rollback_twophase(self, conn, xid, is_prepared): - """Intercept rollback_twophase() events. - - :param conn: :class:`.Connection` object - :param xid: two-phase XID identifier - :param is_prepared: boolean, indicates if - :meth:`.TwoPhaseTransaction.prepare` was called. - - """ - - def commit_twophase(self, conn, xid, is_prepared): - """Intercept commit_twophase() events. - - :param conn: :class:`.Connection` object - :param xid: two-phase XID identifier - :param is_prepared: boolean, indicates if - :meth:`.TwoPhaseTransaction.prepare` was called. - - """ - - -class DialectEvents(event.Events): - """event interface for execution-replacement functions. - - These events allow direct instrumentation and replacement - of key dialect functions which interact with the DBAPI. - - .. note:: - - :class:`.DialectEvents` hooks should be considered **semi-public** - and experimental. - These hooks are not for general use and are only for those situations - where intricate re-statement of DBAPI mechanics must be injected onto - an existing dialect. For general-use statement-interception events, - please use the :class:`.ConnectionEvents` interface. - - .. seealso:: - - :meth:`.ConnectionEvents.before_cursor_execute` - - :meth:`.ConnectionEvents.before_execute` - - :meth:`.ConnectionEvents.after_cursor_execute` - - :meth:`.ConnectionEvents.after_execute` - - - .. versionadded:: 0.9.4 - - """ - - _target_class_doc = "SomeEngine" - _dispatch_target = Dialect - - @classmethod - def _listen(cls, event_key, retval=False): - target = event_key.dispatch_target - - target._has_events = True - event_key.base_listen() - - @classmethod - def _accept_with(cls, target): - if isinstance(target, type): - if issubclass(target, Engine): - return Dialect - elif issubclass(target, Dialect): - return target - elif isinstance(target, Engine): - return target.dialect - else: - return target - - def do_connect(self, dialect, conn_rec, cargs, cparams): - """Receive connection arguments before a connection is made. - - Return a DBAPI connection to halt further events from invoking; - the returned connection will be used. - - Alternatively, the event can manipulate the cargs and/or cparams - collections; cargs will always be a Python list that can be mutated - in-place and cparams a Python dictionary. Return None to - allow control to pass to the next event handler and ultimately - to allow the dialect to connect normally, given the updated - arguments. - - .. versionadded:: 1.0.3 - - """ - - def do_executemany(self, cursor, statement, parameters, context): - """Receive a cursor to have executemany() called. - - Return the value True to halt further events from invoking, - and to indicate that the cursor execution has already taken - place within the event handler. - - """ - - def do_execute_no_params(self, cursor, statement, context): - """Receive a cursor to have execute() with no parameters called. - - Return the value True to halt further events from invoking, - and to indicate that the cursor execution has already taken - place within the event handler. - - """ - - def do_execute(self, cursor, statement, parameters, context): - """Receive a cursor to have execute() called. - - Return the value True to halt further events from invoking, - and to indicate that the cursor execution has already taken - place within the event handler. - - """ - - def do_setinputsizes( - self, inputsizes, cursor, statement, parameters, context - ): - """Receive the setinputsizes dictionary for possible modification. - - This event is emitted in the case where the dialect makes use of the - DBAPI ``cursor.setinputsizes()`` method which passes information about - parameter binding for a particular statement. The given - ``inputsizes`` dictionary will contain :class:`.BindParameter` objects - as keys, linked to DBAPI-specific type objects as values; for - parameters that are not bound, they are added to the dictionary with - ``None`` as the value, which means the parameter will not be included - in the ultimate setinputsizes call. The event may be used to inspect - and/or log the datatypes that are being bound, as well as to modify the - dictionary in place. Parameters can be added, modified, or removed - from this dictionary. Callers will typically want to inspect the - :attr:`.BindParameter.type` attribute of the given bind objects in - order to make decisions about the DBAPI object. - - After the event, the ``inputsizes`` dictionary is converted into - an appropriate datastructure to be passed to ``cursor.setinputsizes``; - either a list for a positional bound parameter execution style, - or a dictionary of string parameter keys to DBAPI type objects for - a named bound parameter execution style. - - Most dialects **do not use** this method at all; the only built-in - dialect which uses this hook is the cx_Oracle dialect. The hook here - is made available so as to allow customization of how datatypes are set - up with the cx_Oracle DBAPI. - - .. versionadded:: 1.2.9 - - .. seealso:: - - :ref:`cx_oracle_setinputsizes` - - """ - pass +from .engine.events import ConnectionEvents # noqa +from .engine.events import DialectEvents # noqa +from .pool.events import PoolEvents # noqa +from .sql.base import SchemaEventTarget # noqa +from .sql.events import DDLEvents # noqa diff --git a/lib/sqlalchemy/pool/__init__.py b/lib/sqlalchemy/pool/__init__.py index ba9871378b..058d595c8b 100644 --- a/lib/sqlalchemy/pool/__init__.py +++ b/lib/sqlalchemy/pool/__init__.py @@ -17,6 +17,7 @@ regular DB-API connect() methods to be transparently managed by a SQLAlchemy connection pool. """ +from . import events # noqa from .base import _ConnectionFairy # noqa from .base import _ConnectionRecord # noqa from .base import _finalize_fairy # noqa diff --git a/lib/sqlalchemy/pool/events.py b/lib/sqlalchemy/pool/events.py new file mode 100644 index 0000000000..7a3a0910af --- /dev/null +++ b/lib/sqlalchemy/pool/events.py @@ -0,0 +1,236 @@ +# sqlalchemy/pool/events.py +# Copyright (C) 2005-2020 the SQLAlchemy authors and contributors +# +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +from .base import Pool +from .. import event +from ..engine.base import Engine + + +class PoolEvents(event.Events): + """Available events for :class:`.Pool`. + + The methods here define the name of an event as well + as the names of members that are passed to listener + functions. + + e.g.:: + + from sqlalchemy import event + + def my_on_checkout(dbapi_conn, connection_rec, connection_proxy): + "handle an on checkout event" + + event.listen(Pool, 'checkout', my_on_checkout) + + In addition to accepting the :class:`.Pool` class and + :class:`.Pool` instances, :class:`.PoolEvents` also accepts + :class:`.Engine` objects and the :class:`.Engine` class as + targets, which will be resolved to the ``.pool`` attribute of the + given engine or the :class:`.Pool` class:: + + engine = create_engine("postgresql://scott:tiger@localhost/test") + + # will associate with engine.pool + event.listen(engine, 'checkout', my_on_checkout) + + """ + + _target_class_doc = "SomeEngineOrPool" + _dispatch_target = Pool + + @classmethod + def _accept_with(cls, target): + if isinstance(target, type): + if issubclass(target, Engine): + return Pool + elif issubclass(target, Pool): + return target + elif isinstance(target, Engine): + return target.pool + else: + return target + + def connect(self, dbapi_connection, connection_record): + """Called at the moment a particular DBAPI connection is first + created for a given :class:`.Pool`. + + This event allows one to capture the point directly after which + the DBAPI module-level ``.connect()`` method has been used in order + to produce a new DBAPI connection. + + :param dbapi_connection: a DBAPI connection. + + :param connection_record: the :class:`._ConnectionRecord` managing the + DBAPI connection. + + """ + + def first_connect(self, dbapi_connection, connection_record): + """Called exactly once for the first time a DBAPI connection is + checked out from a particular :class:`.Pool`. + + The rationale for :meth:`.PoolEvents.first_connect` is to determine + information about a particular series of database connections based + on the settings used for all connections. Since a particular + :class:`.Pool` refers to a single "creator" function (which in terms + of a :class:`.Engine` refers to the URL and connection options used), + it is typically valid to make observations about a single connection + that can be safely assumed to be valid about all subsequent + connections, such as the database version, the server and client + encoding settings, collation settings, and many others. + + :param dbapi_connection: a DBAPI connection. + + :param connection_record: the :class:`._ConnectionRecord` managing the + DBAPI connection. + + """ + + def checkout(self, dbapi_connection, connection_record, connection_proxy): + """Called when a connection is retrieved from the Pool. + + :param dbapi_connection: a DBAPI connection. + + :param connection_record: the :class:`._ConnectionRecord` managing the + DBAPI connection. + + :param connection_proxy: the :class:`._ConnectionFairy` object which + will proxy the public interface of the DBAPI connection for the + lifespan of the checkout. + + If you raise a :class:`~sqlalchemy.exc.DisconnectionError`, the current + connection will be disposed and a fresh connection retrieved. + Processing of all checkout listeners will abort and restart + using the new connection. + + .. seealso:: :meth:`.ConnectionEvents.engine_connect` - a similar event + which occurs upon creation of a new :class:`.Connection`. + + """ + + def checkin(self, dbapi_connection, connection_record): + """Called when a connection returns to the pool. + + Note that the connection may be closed, and may be None if the + connection has been invalidated. ``checkin`` will not be called + for detached connections. (They do not return to the pool.) + + :param dbapi_connection: a DBAPI connection. + + :param connection_record: the :class:`._ConnectionRecord` managing the + DBAPI connection. + + """ + + def reset(self, dbapi_connection, connection_record): + """Called before the "reset" action occurs for a pooled connection. + + This event represents + when the ``rollback()`` method is called on the DBAPI connection + before it is returned to the pool. The behavior of "reset" can + be controlled, including disabled, using the ``reset_on_return`` + pool argument. + + + The :meth:`.PoolEvents.reset` event is usually followed by the + :meth:`.PoolEvents.checkin` event is called, except in those + cases where the connection is discarded immediately after reset. + + :param dbapi_connection: a DBAPI connection. + + :param connection_record: the :class:`._ConnectionRecord` managing the + DBAPI connection. + + .. seealso:: + + :meth:`.ConnectionEvents.rollback` + + :meth:`.ConnectionEvents.commit` + + """ + + def invalidate(self, dbapi_connection, connection_record, exception): + """Called when a DBAPI connection is to be "invalidated". + + This event is called any time the :meth:`._ConnectionRecord.invalidate` + method is invoked, either from API usage or via "auto-invalidation", + without the ``soft`` flag. + + The event occurs before a final attempt to call ``.close()`` on the + connection occurs. + + :param dbapi_connection: a DBAPI connection. + + :param connection_record: the :class:`._ConnectionRecord` managing the + DBAPI connection. + + :param exception: the exception object corresponding to the reason + for this invalidation, if any. May be ``None``. + + .. versionadded:: 0.9.2 Added support for connection invalidation + listening. + + .. seealso:: + + :ref:`pool_connection_invalidation` + + """ + + def soft_invalidate(self, dbapi_connection, connection_record, exception): + """Called when a DBAPI connection is to be "soft invalidated". + + This event is called any time the :meth:`._ConnectionRecord.invalidate` + method is invoked with the ``soft`` flag. + + Soft invalidation refers to when the connection record that tracks + this connection will force a reconnect after the current connection + is checked in. It does not actively close the dbapi_connection + at the point at which it is called. + + .. versionadded:: 1.0.3 + + """ + + def close(self, dbapi_connection, connection_record): + """Called when a DBAPI connection is closed. + + The event is emitted before the close occurs. + + The close of a connection can fail; typically this is because + the connection is already closed. If the close operation fails, + the connection is discarded. + + The :meth:`.close` event corresponds to a connection that's still + associated with the pool. To intercept close events for detached + connections use :meth:`.close_detached`. + + .. versionadded:: 1.1 + + """ + + def detach(self, dbapi_connection, connection_record): + """Called when a DBAPI connection is "detached" from a pool. + + This event is emitted after the detach occurs. The connection + is no longer associated with the given connection record. + + .. versionadded:: 1.1 + + """ + + def close_detached(self, dbapi_connection): + """Called when a detached DBAPI connection is closed. + + The event is emitted before the close occurs. + + The close of a connection can fail; typically this is because + the connection is already closed. If the close operation fails, + the connection is discarded. + + .. versionadded:: 1.1 + + """ diff --git a/lib/sqlalchemy/sql/__init__.py b/lib/sqlalchemy/sql/__init__.py index 2b49392278..6554faaa08 100644 --- a/lib/sqlalchemy/sql/__init__.py +++ b/lib/sqlalchemy/sql/__init__.py @@ -98,6 +98,7 @@ def __go(lcls): from . import base from . import coercions from . import elements + from . import events # noqa from . import selectable from . import schema from . import sqltypes diff --git a/lib/sqlalchemy/sql/events.py b/lib/sqlalchemy/sql/events.py new file mode 100644 index 0000000000..cd48d99fc8 --- /dev/null +++ b/lib/sqlalchemy/sql/events.py @@ -0,0 +1,285 @@ +# sqlalchemy/sql/events.py +# Copyright (C) 2005-2020 the SQLAlchemy authors and contributors +# +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +from .base import SchemaEventTarget +from .. import event + + +class DDLEvents(event.Events): + """ + Define event listeners for schema objects, + that is, :class:`.SchemaItem` and other :class:`.SchemaEventTarget` + subclasses, including :class:`.MetaData`, :class:`.Table`, + :class:`.Column`. + + :class:`.MetaData` and :class:`.Table` support events + specifically regarding when CREATE and DROP + DDL is emitted to the database. + + Attachment events are also provided to customize + behavior whenever a child schema element is associated + with a parent, such as, when a :class:`.Column` is associated + with its :class:`.Table`, when a :class:`.ForeignKeyConstraint` + is associated with a :class:`.Table`, etc. + + Example using the ``after_create`` event:: + + from sqlalchemy import event + from sqlalchemy import Table, Column, Metadata, Integer + + m = MetaData() + some_table = Table('some_table', m, Column('data', Integer)) + + def after_create(target, connection, **kw): + connection.execute("ALTER TABLE %s SET name=foo_%s" % + (target.name, target.name)) + + event.listen(some_table, "after_create", after_create) + + DDL events integrate closely with the + :class:`.DDL` class and the :class:`.DDLElement` hierarchy + of DDL clause constructs, which are themselves appropriate + as listener callables:: + + from sqlalchemy import DDL + event.listen( + some_table, + "after_create", + DDL("ALTER TABLE %(table)s SET name=foo_%(table)s") + ) + + The methods here define the name of an event as well + as the names of members that are passed to listener + functions. + + For all :class:`.DDLEvent` events, the ``propagate=True`` keyword argument + will ensure that a given event handler is propagated to copies of the + object, which are made when using the :meth:`.Table.tometadata` method:: + + from sqlalchemy import DDL + event.listen( + some_table, + "after_create", + DDL("ALTER TABLE %(table)s SET name=foo_%(table)s"), + propagate=True + ) + + new_table = some_table.tometadata(new_metadata) + + The above :class:`.DDL` object will also be associated with the + :class:`.Table` object represented by ``new_table``. + + .. seealso:: + + :ref:`event_toplevel` + + :class:`.DDLElement` + + :class:`.DDL` + + :ref:`schema_ddl_sequences` + + """ + + _target_class_doc = "SomeSchemaClassOrObject" + _dispatch_target = SchemaEventTarget + + def before_create(self, target, connection, **kw): + r"""Called before CREATE statements are emitted. + + :param target: the :class:`.MetaData` or :class:`.Table` + object which is the target of the event. + :param connection: the :class:`.Connection` where the + CREATE statement or statements will be emitted. + :param \**kw: additional keyword arguments relevant + to the event. The contents of this dictionary + may vary across releases, and include the + list of tables being generated for a metadata-level + event, the checkfirst flag, and other + elements used by internal events. + + :func:`.event.listen` also accepts the ``propagate=True`` + modifier for this event; when True, the listener function will + be established for any copies made of the target object, + i.e. those copies that are generated when + :meth:`.Table.tometadata` is used. + + """ + + def after_create(self, target, connection, **kw): + r"""Called after CREATE statements are emitted. + + :param target: the :class:`.MetaData` or :class:`.Table` + object which is the target of the event. + :param connection: the :class:`.Connection` where the + CREATE statement or statements have been emitted. + :param \**kw: additional keyword arguments relevant + to the event. The contents of this dictionary + may vary across releases, and include the + list of tables being generated for a metadata-level + event, the checkfirst flag, and other + elements used by internal events. + + :func:`.event.listen` also accepts the ``propagate=True`` + modifier for this event; when True, the listener function will + be established for any copies made of the target object, + i.e. those copies that are generated when + :meth:`.Table.tometadata` is used. + + """ + + def before_drop(self, target, connection, **kw): + r"""Called before DROP statements are emitted. + + :param target: the :class:`.MetaData` or :class:`.Table` + object which is the target of the event. + :param connection: the :class:`.Connection` where the + DROP statement or statements will be emitted. + :param \**kw: additional keyword arguments relevant + to the event. The contents of this dictionary + may vary across releases, and include the + list of tables being generated for a metadata-level + event, the checkfirst flag, and other + elements used by internal events. + + :func:`.event.listen` also accepts the ``propagate=True`` + modifier for this event; when True, the listener function will + be established for any copies made of the target object, + i.e. those copies that are generated when + :meth:`.Table.tometadata` is used. + + """ + + def after_drop(self, target, connection, **kw): + r"""Called after DROP statements are emitted. + + :param target: the :class:`.MetaData` or :class:`.Table` + object which is the target of the event. + :param connection: the :class:`.Connection` where the + DROP statement or statements have been emitted. + :param \**kw: additional keyword arguments relevant + to the event. The contents of this dictionary + may vary across releases, and include the + list of tables being generated for a metadata-level + event, the checkfirst flag, and other + elements used by internal events. + + :func:`.event.listen` also accepts the ``propagate=True`` + modifier for this event; when True, the listener function will + be established for any copies made of the target object, + i.e. those copies that are generated when + :meth:`.Table.tometadata` is used. + + """ + + def before_parent_attach(self, target, parent): + """Called before a :class:`.SchemaItem` is associated with + a parent :class:`.SchemaItem`. + + :param target: the target object + :param parent: the parent to which the target is being attached. + + :func:`.event.listen` also accepts the ``propagate=True`` + modifier for this event; when True, the listener function will + be established for any copies made of the target object, + i.e. those copies that are generated when + :meth:`.Table.tometadata` is used. + + """ + + def after_parent_attach(self, target, parent): + """Called after a :class:`.SchemaItem` is associated with + a parent :class:`.SchemaItem`. + + :param target: the target object + :param parent: the parent to which the target is being attached. + + :func:`.event.listen` also accepts the ``propagate=True`` + modifier for this event; when True, the listener function will + be established for any copies made of the target object, + i.e. those copies that are generated when + :meth:`.Table.tometadata` is used. + + """ + + def column_reflect(self, inspector, table, column_info): + """Called for each unit of 'column info' retrieved when + a :class:`.Table` is being reflected. + + The dictionary of column information as returned by the + dialect is passed, and can be modified. The dictionary + is that returned in each element of the list returned + by :meth:`.reflection.Inspector.get_columns`: + + * ``name`` - the column's name + + * ``type`` - the type of this column, which should be an instance + of :class:`~sqlalchemy.types.TypeEngine` + + * ``nullable`` - boolean flag if the column is NULL or NOT NULL + + * ``default`` - the column's server default value. This is + normally specified as a plain string SQL expression, however the + event can pass a :class:`.FetchedValue`, :class:`.DefaultClause`, + or :func:`.sql.expression.text` object as well. + + .. versionchanged:: 1.1.6 + + The :meth:`.DDLEvents.column_reflect` event allows a non + string :class:`.FetchedValue`, + :func:`.sql.expression.text`, or derived object to be + specified as the value of ``default`` in the column + dictionary. + + * ``attrs`` - dict containing optional column attributes + + The event is called before any action is taken against + this dictionary, and the contents can be modified. + The :class:`.Column` specific arguments ``info``, ``key``, + and ``quote`` can also be added to the dictionary and + will be passed to the constructor of :class:`.Column`. + + Note that this event is only meaningful if either + associated with the :class:`.Table` class across the + board, e.g.:: + + from sqlalchemy.schema import Table + from sqlalchemy import event + + def listen_for_reflect(inspector, table, column_info): + "receive a column_reflect event" + # ... + + event.listen( + Table, + 'column_reflect', + listen_for_reflect) + + ...or with a specific :class:`.Table` instance using + the ``listeners`` argument:: + + def listen_for_reflect(inspector, table, column_info): + "receive a column_reflect event" + # ... + + t = Table( + 'sometable', + autoload=True, + listeners=[ + ('column_reflect', listen_for_reflect) + ]) + + This because the reflection process initiated by ``autoload=True`` + completes within the scope of the constructor for :class:`.Table`. + + :func:`.event.listen` also accepts the ``propagate=True`` + modifier for this event; when True, the listener function will + be established for any copies made of the target object, + i.e. those copies that are generated when + :meth:`.Table.tometadata` is used. + + """ diff --git a/lib/sqlalchemy/sql/naming.py b/lib/sqlalchemy/sql/naming.py index 756a85b22a..f530177dd2 100644 --- a/lib/sqlalchemy/sql/naming.py +++ b/lib/sqlalchemy/sql/naming.py @@ -12,6 +12,7 @@ import re +from . import events # noqa from .elements import _defer_name from .elements import _defer_none_name from .elements import conv @@ -24,7 +25,6 @@ from .schema import PrimaryKeyConstraint from .schema import Table from .schema import UniqueConstraint from .. import event -from .. import events # noqa from .. import exc diff --git a/lib/sqlalchemy/sql/traversals.py b/lib/sqlalchemy/sql/traversals.py index 68a3a07497..03ff7c4394 100644 --- a/lib/sqlalchemy/sql/traversals.py +++ b/lib/sqlalchemy/sql/traversals.py @@ -5,8 +5,8 @@ import operator from . import operators from .visitors import ExtendedInternalTraversal from .visitors import InternalTraversal -from .. import inspect from .. import util +from ..inspection import inspect SKIP_TRAVERSE = util.symbol("skip_traverse") COMPARE_FAILED = False -- 2.47.3