]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
- Added a new feature :attr:`.Config.attributes`, to help with the use
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 24 Jan 2015 01:05:02 +0000 (20:05 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 24 Jan 2015 01:05:02 +0000 (20:05 -0500)
case of sharing state such as engines and connections on the outside
with a series of Alembic API calls; also added a new cookbook section
to describe this simple but pretty important use case.

alembic/config.py
alembic/templates/generic/env.py
alembic/templates/pylons/env.py
alembic/util.py
docs/build/api.rst
docs/build/autogenerate.rst
docs/build/changelog.rst
docs/build/cookbook.rst
tests/test_config.py

index 27bb31a0728dff20c5fa674c7dde6b2e765673c9..7f813d27bf4d5eed87e24cb5b2c8f91a901379ef 100644 (file)
@@ -40,6 +40,13 @@ class Config(object):
         alembic_cfg.set_main_option("url", "postgresql://foo/bar")
         alembic_cfg.set_section_option("mysection", "foo", "bar")
 
+    For passing non-string values to environments, such as connections and
+    engines, use the :attr:`.Config.attributes` dictionary::
+
+        with engine.begin() as connection:
+            alembic_cfg.attributes['connection'] = connection
+            command.upgrade(alembic_cfg, "head")
+
     :param file_: name of the .ini file to open.
     :param ini_section: name of the main Alembic section within the
      .ini file
@@ -49,7 +56,7 @@ class Config(object):
     :param stdout: buffer where the "print" output of commands will be sent.
      Defaults to ``sys.stdout``.
 
-     ..versionadded:: 0.4
+     .. versionadded:: 0.4
 
     :param config_args: A dictionary of keys and values that will be used
      for substitution in the alembic config file.  The dictionary as given
@@ -59,13 +66,22 @@ class Config(object):
      dictionary before the dictionary is passed to ``SafeConfigParser()``
      to parse the .ini file.
 
-     ..versionadded:: 0.7.0
+     .. versionadded:: 0.7.0
+
+    :param attributes: optional dictionary of arbitrary Python keys/values,
+     which will be populated into the :attr:`.Config.attributes` dictionary.
+
+     .. versionadded:: 0.7.5
+
+     .. seealso::
+
+        :ref:`connection_sharing`
 
     """
 
     def __init__(self, file_=None, ini_section='alembic', output_buffer=None,
                  stdout=sys.stdout, cmd_opts=None,
-                 config_args=util.immutabledict()):
+                 config_args=util.immutabledict(), attributes=None):
         """Construct a new :class:`.Config`
 
         """
@@ -75,6 +91,8 @@ class Config(object):
         self.stdout = stdout
         self.cmd_opts = cmd_opts
         self.config_args = dict(config_args)
+        if attributes:
+            self.attributes.update(attributes)
 
     cmd_opts = None
     """The command-line options passed to the ``alembic`` script.
@@ -101,6 +119,28 @@ class Config(object):
 
     """
 
+    @util.memoized_property
+    def attributes(self):
+        """A Python dictionary for storage of additional state.
+
+
+        This is a utility dictionary which can include not just strings but
+        engines, connections, schema objects, or anything else.
+        Use this to pass objects into an env.py script, such as passing
+        a :class:`.Connection` when calling
+        commands from :mod:`alembic.command` programmatically.
+
+        .. versionadded:: 0.7.5
+
+        .. seealso::
+
+            :ref:`connection_sharing`
+
+            :paramref:`.Config.attributes`
+
+        """
+        return {}
+
     def print_stdout(self, text, *arg):
         """Render a message to standard out."""
 
index fccd445a69a856834a01aa7234df31b90a1b8e0f..280006d5fc9b752d29ddf859acce0db5a8538e01 100644 (file)
@@ -49,22 +49,19 @@ def run_migrations_online():
     and associate a connection with the context.
 
     """
-    engine = engine_from_config(
+    connectable = engine_from_config(
         config.get_section(config.config_ini_section),
         prefix='sqlalchemy.',
         poolclass=pool.NullPool)
 
-    connection = engine.connect()
-    context.configure(
-        connection=connection,
-        target_metadata=target_metadata
-    )
+    with connectable.connect() as connection:
+        context.configure(
+            connection=connection,
+            target_metadata=target_metadata
+        )
 
-    try:
         with context.begin_transaction():
             context.run_migrations()
-    finally:
-        connection.close()
 
 if context.is_offline_mode():
     run_migrations_offline()
index 332942822d53a7b43de74853626cf0565b18a04b..70eea4e0b8669f8d6e45fdc7fe264e0e979c750a 100644 (file)
@@ -62,23 +62,14 @@ def run_migrations_online():
     # engine = meta.engine
     raise NotImplementedError("Please specify engine connectivity here")
 
-    if isinstance(engine, Engine):
-        connection = engine.connect()
-    else:
-        raise Exception(
-            'Expected engine instance got %s instead' % type(engine)
+    with engine.connect() as connection:
+        context.configure(
+            connection=connection,
+            target_metadata=target_metadata
         )
 
-    context.configure(
-        connection=connection,
-        target_metadata=target_metadata
-    )
-
-    try:
         with context.begin_transaction():
             context.run_migrations()
-    finally:
-        connection.close()
 
 if context.is_offline_mode():
     run_migrations_offline()
index d9ec1c8146f1f94de4b297caa7ebabeea773d71b..87bc7b1cbd5d60ae60dfda746e0dadbf74a23ae7 100644 (file)
@@ -323,7 +323,7 @@ class memoized_property(object):
 
     def __get__(self, obj, cls):
         if obj is None:
-            return None
+            return self
         obj.__dict__[self.__name__] = result = self.fget(obj)
         return result
 
index 48da80528e431c17b15fc88eab52ed8790c37960..fea4e1478e6b9ca6cc44ad22623b6e83051e5110 100644 (file)
@@ -87,6 +87,19 @@ object, as in::
     alembic_cfg = Config("/path/to/yourapp/alembic.ini")
     command.upgrade(alembic_cfg, "head")
 
+In many cases, and perhaps more often than not, an application will wish
+to call upon a series of Alembic commands and/or other features.  It is
+usually a good idea to link multiple commands along a single connection
+and transaction, if feasible.  This can be achieved using the
+:attr:`.Config.attributes` dictionary in order to share a connection::
+
+    with engine.begin() as connection:
+        alembic_cfg.attributes['connection'] = connection
+        command.upgrade(alembic_cfg, "head")
+
+This recipe requires that ``env.py`` consumes this connection argument;
+see the example in :ref:`connection_sharing` for details.
+
 To write small API functions that make direct use of database and script directory
 information, rather than just running one of the built-in commands,
 use the :class:`.ScriptDirectory` and :class:`.MigrationContext`
index ee9ccb91ce304619fcffbb775accfcd19d9b2332..8ad79ede7a04ab3053f5fb38d9379279aa3f843e 100644 (file)
@@ -35,19 +35,14 @@ we can see the directive passed to :meth:`.EnvironmentContext.configure`::
         engine = engine_from_config(
                     config.get_section(config.config_ini_section), prefix='sqlalchemy.')
 
-        connection = engine.connect()
-        context.configure(
-                    connection=connection,
-                    target_metadata=target_metadata
-                    )
-
-        trans = connection.begin()
-        try:
-            context.run_migrations()
-            trans.commit()
-        except:
-            trans.rollback()
-            raise
+        with engine.connect() as connection:
+            context.configure(
+                        connection=connection,
+                        target_metadata=target_metadata
+                        )
+
+            with context.begin_transaction():
+                context.run_migrations()
 
 We can then use the ``alembic revision`` command in conjunction with the
 ``--autogenerate`` option.  Suppose
index f9c61455b1181931e92a129b4c5daf6e74d5b17a..225db4473f9b357359a6e754a34a4dbe6d5ede72 100644 (file)
@@ -6,6 +6,27 @@ Changelog
 .. changelog::
     :version: 0.7.5
 
+    .. change::
+      :tags: feature, commands
+
+      Added a new feature :attr:`.Config.attributes`, to help with the use
+      case of sharing state such as engines and connections on the outside
+      with a series of Alembic API calls; also added a new cookbook section
+      to describe this simple but pretty important use case.
+
+      .. seealso::
+
+          :ref:`connection_sharing`
+
+    .. change::
+      :tags: feature, environment
+
+      The format of the default ``env.py`` script has been refined a bit;
+      it now uses context managers not only for the scope of the transaction,
+      but also for connectivity from the starting engine.  The engine is also
+      now called a "connectable" in support of the use case of an external
+      connection being passed in.
+
     .. change::
       :tags: feature, versioning
       :tickets: 267
index d24aab9eca653b6808819b1026e276e3f00573fd..8c1e0d71467af892ac4f706a924aa677c094e3fc 100644 (file)
@@ -187,3 +187,77 @@ To invoke our migrations with data included, we use the ``-x`` flag::
 The :meth:`.EnvironmentContext.get_x_argument` is an easy way to support
 new commandline options within environment and migration scripts.
 
+.. _connection_sharing:
+
+Sharing a Connection with a Series of Migration Commands and Environments
+=========================================================================
+
+It is often the case that an application will need to call upon a series
+of commands within :mod:`alembic.command`, where it would be advantageous
+for all operations to proceed along a single transaction.   The connectivity
+for a migration is typically solely determined within the ``env.py`` script
+of a migration environment, which is called within the scope of a command.
+
+The steps to take here are:
+
+1. Produce the :class:`~sqlalchemy.engine.Connection` object to use.
+
+2. Place it somewhere that ``env.py`` will be able to access it.  This
+   can be either a. a module-level global somewhere, or b.
+   an attribute which we place into the :attr:`.Config.attributes`
+   dictionary (if we are on an older Alembic version, we may also attach
+   an attribute directly to the :class:`.Config` object).
+
+3. The ``env.py`` script is modified such that it looks for this
+   :class:`~sqlalchemy.engine.Connection` and makes use of it, in lieu
+   of building up its own :class:`~sqlalchemy.engine.Engine` instance.
+
+We illustrate using :attr:`.Config.attributes`::
+
+    from alembic import command, config
+
+    cfg = config.Config("/path/to/yourapp/alembic.ini")
+    with engine.begin() as connection:
+        cfg.attributes['connection'] = connection
+        command.upgrade(cfg, "head")
+
+Then in ``env.py``::
+
+    def run_migrations_online():
+        connectable = config.attributes.get('connection', None)
+
+        if connectable is None:
+            # only create Engine if we don't have a Connection
+            # from the outside
+            connectable = engine_from_config(
+                config.get_section(config.config_ini_section),
+                prefix='sqlalchemy.',
+                poolclass=pool.NullPool)
+
+        # when connectable is already a Connection object, calling
+        # connect() gives us a *branched connection*.
+
+        with connectable.connect() as connection:
+            context.configure(
+                connection=connection,
+                target_metadata=target_metadata
+            )
+
+            with context.begin_transaction():
+                context.run_migrations()
+
+.. topic:: Branched Connections
+
+    Note that we are calling the ``connect()`` method, **even if we are
+    using a** :class:`~sqlalchemy.engine.Connection` **object to start with**.
+    The effect this has when calling :meth:`~sqlalchemy.engine.Connection.connect`
+    is that SQLAlchemy passes us a **branch** of the original connection; it
+    is in every way the same as the :class:`~sqlalchemy.engine.Connection`
+    we started with, except it provides **nested scope**; the
+    context we have here as well as the
+    :meth:`~sqlalchemy.engine.Connection.close` method of this branched
+    connection doesn't actually close the outer connection, which stays
+    active for continued use.
+
+.. versionadded:: 0.7.5 Added :attr:`.Config.attributes`.
+
index 2d8f964cd2f9ebae32473dde20e9fa2f5ce6ee5d..db37456d724f4cfe24db5de6c4102666f24658af 100644 (file)
@@ -67,6 +67,23 @@ class ConfigTest(TestBase):
             ScriptDirectory.from_config, cfg
         )
 
+    def test_attributes_attr(self):
+        m1 = Mock()
+        cfg = config.Config()
+        cfg.attributes['connection'] = m1
+        eq_(
+            cfg.attributes['connection'], m1
+        )
+
+    def test_attributes_construtor(self):
+        m1 = Mock()
+        m2 = Mock()
+        cfg = config.Config(attributes={'m1': m1})
+        cfg.attributes['connection'] = m2
+        eq_(
+            cfg.attributes, {'m1': m1, 'connection': m2}
+        )
+
 
 class StdoutOutputEncodingTest(TestBase):