]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
turn alembic.op and alembic.context into real proxy modules,
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 26 Jan 2012 20:43:57 +0000 (15:43 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 26 Jan 2012 20:43:57 +0000 (15:43 -0500)
with an accurate system of reflecting the Operations and
EnvironmentContext methods into them.

alembic/__init__.py
alembic/context.py [new file with mode: 0644]
alembic/environment.py
alembic/op.py
alembic/operations.py
alembic/util.py
docs/build/api.rst
docs/build/front.rst
tests/test_op.py

index 09d91dfaca4d7c3b5b4e799b0539f972eabbfc97..dd0591ec0bd9060e4b20e7a51699430c353aa29b 100644 (file)
@@ -6,10 +6,5 @@ package_dir = path.abspath(path.dirname(__file__))
 
 
 from alembic import op
-
-class _ContextProxy(object):
-    """A proxy object for the current :class:`.EnvironmentContext`."""
-    def __getattr__(self, key):
-        return getattr(_context, key)
-context = _ContextProxy()
+from alembic import context
 
diff --git a/alembic/context.py b/alembic/context.py
new file mode 100644 (file)
index 0000000..6c33d85
--- /dev/null
@@ -0,0 +1,6 @@
+from alembic.environment import EnvironmentContext
+from alembic import util
+
+# create proxy functions for 
+# each method on the EnvironmentContext class.
+util.create_module_class_proxy(EnvironmentContext, globals(), locals())
index f61c9c7b9587a27839d5a451534a7553a5b3b505..5e86fcd34fc3ef72e3f0cbfd35e6fadd2318e32c 100644 (file)
@@ -46,12 +46,12 @@ class EnvironmentContext(object):
         be made available as ``from alembic import context``.
     
         """
-        alembic._context = self
+        alembic.context._install_proxy(self)
         return self
 
     def __exit__(self, *arg, **kw):
-        alembic._context = None
-        alembic.op._proxy = None
+        alembic.context._remove_proxy()
+        alembic.op._remove_proxy()
 
     def is_offline_mode(self):
         """Return True if the current migrations environment 
@@ -78,7 +78,7 @@ class EnvironmentContext(object):
         made available via :meth:`.configure`.
 
         """
-        return self.migration_context.impl.transactional_ddl
+        return self.get_context().impl.transactional_ddl
 
     def requires_connection(self):
         return not self.is_offline_mode()
@@ -105,7 +105,7 @@ class EnvironmentContext(object):
 
         """
         if self._migration_context is not None:
-            return self.script._as_rev_number(self.migration_context._start_from_rev)
+            return self.script._as_rev_number(self.get_context()._start_from_rev)
         elif 'starting_rev' in self.context_opts:
             return self.script._as_rev_number(self.context_opts['starting_rev'])
         else:
@@ -345,7 +345,7 @@ class EnvironmentContext(object):
 
         """
         with Operations.context(self._migration_context):
-            self.migration_context.run_migrations(**kw)
+            self.get_context().run_migrations(**kw)
 
     def execute(self, sql):
         """Execute the given SQL using the current change context.
@@ -359,7 +359,7 @@ class EnvironmentContext(object):
         made available via :meth:`.configure`.
 
         """
-        self.migration_context.execute(sql)
+        self.get_context().execute(sql)
 
     def static_output(self, text):
         """Emit text directly to the "offline" SQL stream.
@@ -370,7 +370,7 @@ class EnvironmentContext(object):
         is added, etc.
     
         """
-        self.migration_context.impl.static_output(text)
+        self.get_context().impl.static_output(text)
 
     def begin_transaction(self):
         """Return a context manager that will 
@@ -423,30 +423,25 @@ class EnvironmentContext(object):
         elif self.is_offline_mode():
             @contextmanager
             def begin_commit():
-                self.migration_context.impl.emit_begin()
+                self.get_context().impl.emit_begin()
                 yield
-                self.migration_context.impl.emit_commit()
+                self.get_context().impl.emit_commit()
             return begin_commit()
         else:
             return self.get_bind().begin()
 
-    @property
-    def migration_context(self):
+    def get_context(self):
         """Return the current :class:`.MigrationContext` object.
 
         If :meth:`.EnvironmentContext.configure` has not been called yet, raises
         an exception.
 
         """
+
         if self._migration_context is None:
             raise Exception("No context has been configured yet.")
         return self._migration_context
 
-    def get_context(self):
-        """A synonym for :attr:`.EnvironmentContext.migration_context`."""
-
-        return self.migration_context
-
     def get_bind(self):
         """Return the current 'bind'.
 
@@ -458,9 +453,9 @@ class EnvironmentContext(object):
         made available via :meth:`.configure`.
 
         """
-        return self.migration_context.bind
+        return self.get_context().bind
 
     def get_impl(self):
-        return self.migration_context.impl
+        return self.get_context().impl
 
 configure = EnvironmentContext
index 8a5e0fa05247205afc3b7ec6649d208ef907a48b..9f2a26b445276f764c541c32914540114a03e792 100644 (file)
@@ -1,19 +1,6 @@
 from alembic.operations import Operations
+from alembic import util
 
 # create proxy functions for 
 # each method on the Operations class.
-
-# TODO: this is a quick and dirty version of this.
-# Ideally, we'd be duplicating method signatures 
-# and such, using eval(), etc.
-
-_proxy = None
-def _create_op_proxy(name):
-    def go(*arg, **kw):
-        return getattr(_proxy, name)(*arg, **kw)
-    go.__name__ = name
-    return go
-
-for methname in dir(Operations):
-    if not methname.startswith('_'):
-        locals()[methname] = _create_op_proxy(methname)
\ No newline at end of file
+util.create_module_class_proxy(Operations, globals(), locals())
index f3e6708c440f182c1ad342630a52675b92fd4412..9efa830ed0e46ee7495434329e1e73fcac3eb9c3 100644 (file)
@@ -37,9 +37,9 @@ class Operations(object):
     @contextmanager
     def context(cls, migration_context):
         op = Operations(migration_context)
-        alembic.op._proxy = op
+        alembic.op._install_proxy(op)
         yield op
-        del alembic.op._proxy
+        alembic.op._remove_proxy()
 
     def _foreign_key_constraint(self, name, source, referent, local_cols, remote_cols):
         m = schema.MetaData()
index f58992a0e306a5b3fd472e9cef3514dc50abfb25..3ae15f911cb80e18db3832cbb22d6c45f8bfdfd5 100644 (file)
@@ -5,9 +5,11 @@ import sys
 import os
 import textwrap
 from sqlalchemy.engine import url
+from sqlalchemy import util as sqla_util
 import imp
 import warnings
 import re
+import inspect
 import time
 import random
 import uuid
@@ -42,6 +44,72 @@ def template_to_file(template_file, dest, **kw):
             Template(filename=template_file).render(**kw)
         )
 
+def create_module_class_proxy(cls, globals_, locals_):
+    """Create module level proxy functions for the
+    methods on a given class.
+    
+    The functions will have a compatible signature
+    as the methods.   A proxy is established
+    using the ``_install_proxy(obj)`` function,
+    and removed using ``_remove_proxy()``, both
+    installed by calling this function.
+
+    """
+    attr_names = set()
+
+    def _install_proxy(obj):
+        globals_['_proxy'] = obj
+        for name in attr_names:
+            globals_[name] = getattr(obj, name)
+
+    def _remove_proxy():
+        globals_['_proxy'] = None
+        for name in attr_names:
+            del globals_[name]
+
+    globals_['_install_proxy'] = _install_proxy
+    globals_['_remove_proxy'] = _remove_proxy
+
+    def _create_op_proxy(name):
+        fn = getattr(cls, name)
+        spec = inspect.getargspec(fn)
+        if spec[0] and spec[0][0] == 'self':
+            spec[0].pop(0)
+        args = inspect.formatargspec(*spec)
+        num_defaults = 0
+        if spec[3]:
+            num_defaults += len(spec[3])
+        name_args = spec[0]
+        if num_defaults:
+            defaulted_vals = name_args[0-num_defaults:]
+        else:
+            defaulted_vals = ()
+
+        apply_kw = inspect.formatargspec(
+                                name_args, spec[1], spec[2], 
+                                defaulted_vals,
+                                formatvalue=lambda x: '=' + x)
+
+        func_text = textwrap.dedent("""\
+        def %(name)s(%(args)s):
+            %(doc)r
+            return _proxy.%(name)s(%(apply_kw)s)
+        """ % {
+            'name':name,
+            'args':args[1:-1],
+            'apply_kw':apply_kw[1:-1],
+            'doc':fn.__doc__,
+        })
+        lcl = {}
+        exec func_text in globals_, lcl
+        return lcl[name]
+
+    for methname in dir(cls):
+        if not methname.startswith('_'):
+            if callable(getattr(cls, methname)):
+                locals_[methname] = _create_op_proxy(methname)
+            else:
+                attr_names.add(methname)
 
 def status(_statmsg, fn, *arg, **kw):
     msg(_statmsg + "...", False)
index 3abc955695ecf8e253a2d9bebeb9c28090bdc1e1..73e094853192fa345be6bdef044b21dd59f63655 100644 (file)
@@ -14,7 +14,7 @@ and :class:`.Operations` classes, pictured below.
 .. image:: api_overview.png
 
 An Alembic command begins by instantiating an :class:`.EnvironmentContext` object, then
-making it available via the ``alembic.context`` datamember.  The ``env.py``
+making it available via the ``alembic.context`` proxy module.  The ``env.py``
 script, representing a user-configurable migration environment, is then 
 invoked.   The ``env.py`` script is then responsible for calling upon the
 :meth:`.EnvironmentContext.configure`, whose job it is to create 
@@ -34,7 +34,7 @@ via the :attr:`.EnvironmentContext.migration_context` datamember.
 Finally, ``env.py`` calls upon the :meth:`.EnvironmentContext.run_migrations`
 method.   Within this method, a new :class:`.Operations` object, which
 provides an API for individual database migration operations, is established
-within the ``alembic.op`` datamember.   The :class:`.Operations` object
+within the ``alembic.op`` proxy module.   The :class:`.Operations` object
 uses the :class:`.MigrationContext` object ultimately as a source of 
 database connectivity, though in such a way that it does not care if the
 :class:`.MigrationContext` is talking to a real database or just writing
index 3399d72cd6705d9ad8a45f47ee95c5e089d93abd..bbc6437348f47a4497d09bf9ec27cb98754c3662 100644 (file)
@@ -56,25 +56,6 @@ Upgrading from Alembic 0.1 to 0.2
 Alembic 0.2 has some reorganizations and features that might impact an existing 0.1
 installation.   These include:
 
-* The ``alembic.op`` module is now generated from a class called
-  :class:`.Operations`, including standalone functions that each proxy
-  to the current instance of :class:`.Operations`.   The behavior here
-  is tailored such that an existing migration script that imports
-  symbols directly from ``alembic.op``, that is, 
-  ``from alembic.op import create_table``, should still work fine; though ideally
-  it's better to use the style ``from alembic import op``, then call
-  migration methods directly from the ``op`` member.  The functions inside
-  of ``alembic.op`` are at the moment minimally tailored proxies; a future
-  release should refine these to more closely resemble the :class:`.Operations`
-  methods they represent.
-* The ``alembic.context`` module no longer exists, instead ``alembic.context``
-  is an object inside the ``alembic`` module which proxies to an underlying
-  instance of :class:`.EnvironmentContext`.  :class:`.EnvironmentContext`
-  represents the current environment in an encapsulated way.   Most ``env.py``
-  scripts that don't import from the ``alembic.context`` name directly,
-  instead importing ``context`` itself, should be fine here.   A script that attempts to
-  import from it, such as ``from alembic.context import configure``, will
-  need to be changed to read ``from alembic import context; context.configure()``.
 * The naming convention for migration files is now customizable, and defaults
   to the scheme "%(rev)s_%(slug)s", where "slug" is based on the message
   added to the script.   When Alembic reads one of these files, it looks
@@ -92,7 +73,13 @@ installation.   These include:
   unless you are renaming them.  Alembic will fall back to pulling in the version 
   identifier from the filename if ``revision`` isn't present, as long as the 
   filename uses the old naming convention.
-
+* The ``alembic.op`` and ``alembic.context`` modules are now generated
+  as a collection of proxy functions, which when used refer to an
+  object instance installed when migrations run.  ``alembic.op`` refers to 
+  an instance of the :class:`.Operations` object, and ``alembic.context`` refers to 
+  an instance of the :class:`.EnvironmentContext` object.  Most existing
+  setups should be able to run with no changes, as the functions are 
+  established at module load time and remain fully importable.
 
 Community
 =========
index c85869a85f5884036f1e099159fb45a6528c3862..8af4711002e7bbee93d42325a0f4de136f77152f 100644 (file)
@@ -7,6 +7,7 @@ from sqlalchemy import Integer, Column, ForeignKey, \
             Boolean
 from sqlalchemy.sql import table, column, func
 
+
 def test_rename_table():
     context = op_fixture()
     op.rename_table('t1', 't2')