]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
- tried it out in my work project, and realized the "op" and "context" namespaces
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 24 Jan 2012 23:10:34 +0000 (18:10 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 24 Jan 2012 23:10:34 +0000 (18:10 -0500)
need to be there fully and in particular "context" needs to be
a proxy object, as env.py may have dependencies which live beyond the
scope of the migration script.   Will have to try to make
these proxies as straightforward as possible.
- more architecture docs

alembic/__init__.py
alembic/environment.py
alembic/op.py [new file with mode: 0644]
alembic/operations.py
docs/build/api_overview.png
docs/build/assets/api_overview.graffle
docs/build/front.rst
docs/build/ops.rst

index e561d0d9ea4a85aee93446a740736ed1bcc3db38..09d91dfaca4d7c3b5b4e799b0539f972eabbfc97 100644 (file)
@@ -5,8 +5,11 @@ __version__ = '0.2.0'
 package_dir = path.abspath(path.dirname(__file__))
 
 
-class _OpProxy(object):
-    _proxy = None
+from alembic import op
+
+class _ContextProxy(object):
+    """A proxy object for the current :class:`.EnvironmentContext`."""
     def __getattr__(self, key):
-        return getattr(self._proxy, key)
-op = _OpProxy()
+        return getattr(_context, key)
+context = _ContextProxy()
+
index d562add6c12b18e97b1d9c83c6d64db773b42412..f61c9c7b9587a27839d5a451534a7553a5b3b505 100644 (file)
@@ -46,11 +46,11 @@ class EnvironmentContext(object):
         be made available as ``from alembic import context``.
     
         """
-        alembic.context = self
+        alembic._context = self
         return self
 
     def __exit__(self, *arg, **kw):
-        del alembic.context
+        alembic._context = None
         alembic.op._proxy = None
 
     def is_offline_mode(self):
diff --git a/alembic/op.py b/alembic/op.py
new file mode 100644 (file)
index 0000000..8a5e0fa
--- /dev/null
@@ -0,0 +1,19 @@
+from alembic.operations import Operations
+
+# 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
index 18b8e69294ff6986548d9e28d640a1a1aadf9e62..f3e6708c440f182c1ad342630a52675b92fd4412 100644 (file)
@@ -9,10 +9,10 @@ __all__ = ('Operations',)
 
 class Operations(object):
     """Define high level migration operations.
-    
+
     Each operation corresponds to some schema migration operation,
     executed against a particular :class:`.MigrationContext`.
-    
+
     Normally, the :class:`.MigrationContext` is created
     within an ``env.py`` script via the
     :meth:`.EnvironmentContext.configure` method.  However,
@@ -21,14 +21,14 @@ class Operations(object):
     class - only :class:`.MigrationContext`, which represents
     connectivity to a single database, is needed
     to use the directives.
-    
+
     """
     def __init__(self, migration_context):
         """Construct a new :class:`.Operations`
-        
+
         :param migration_context: a :class:`.MigrationContext` 
          instance.
-         
+
         """
         self.migration_context = migration_context
         self.impl = migration_context.impl
@@ -111,13 +111,21 @@ class Operations(object):
             if cname not in rel_t.c:
                 rel_t.append_column(schema.Column(cname, NULLTYPE))
 
+    def get_context(self):
+        """Return the :class:`.MigrationsContext` object that's
+        currently in use.
+
+        """
+
+        return self.migration_context
+
     def rename_table(self, old_table_name, new_table_name, schema=None):
         """Emit an ALTER TABLE to rename a table.
-    
+
         :param old_table_name: old name.
         :param new_table_name: new name.
         :param schema: Optional, name of schema to operate within.
-    
+
         """
         self.impl.rename_table(
             old_table_name,
@@ -136,21 +144,21 @@ class Operations(object):
     ):
         """Issue an "alter column" instruction using the 
         current migration context.
-    
+
         Generally, only that aspect of the column which
         is being changed, i.e. name, type, nullability,
         default, needs to be specified.  Multiple changes
         can also be specified at once and the backend should
         "do the right thing", emitting each change either
         separately or together as the backend allows.
-    
+
         MySQL has special requirements here, since MySQL
         cannot ALTER a column without a full specification.
         When producing MySQL-compatible migration files,
         it is recommended that the ``existing_type``,
         ``existing_server_default``, and ``existing_nullable``
         parameters be present, if not being altered.
-    
+
         Type changes which are against the SQLAlchemy
         "schema" types :class:`~sqlalchemy.types.Boolean`
         and  :class:`~sqlalchemy.types.Enum` may also
@@ -159,7 +167,7 @@ class Operations(object):
         The ``existing_server_default`` argument is 
         used in this case as well to remove a previous
         constraint.
-    
+
         :param table_name: string name of the target table.
         :param column_name: string name of the target column, 
          as it exists before the operation begins.
@@ -168,12 +176,12 @@ class Operations(object):
         :param server_default: Optional; specify a string 
          SQL expression, :func:`~sqlalchemy.sql.expression.text`,
          or :class:`~sqlalchemy.schema.DefaultClause` to indicate
-         an alteration to the column's default value.  
+         an alteration to the column's default value.
          Set to ``None`` to have the default removed.
         :param name: Optional; specify a string name here to
          indicate the new name within a column rename operation.
         :param type_: Optional; a :class:`~sqlalchemy.types.TypeEngine`
-         type object to specify a change to the column's type.  
+         type object to specify a change to the column's type.
          For SQLAlchemy types that also indicate a constraint (i.e. 
          :class:`~sqlalchemy.types.Boolean`, :class:`~sqlalchemy.types.Enum`), 
          the constraint is also generated.
@@ -220,7 +228,7 @@ class Operations(object):
 
     def add_column(self, table_name, column):
         """Issue an "add column" instruction using the current migration context.
-    
+
         e.g.::
 
             from alembic import op
@@ -228,25 +236,25 @@ class Operations(object):
 
             op.add_column('organization', 
                 Column('name', String())
-            )        
+            )
 
         The provided :class:`~sqlalchemy.schema.Column` object can also
         specify a :class:`~sqlalchemy.schema.ForeignKey`, referencing
         a remote table name.  Alembic will automatically generate a stub
         "referenced" table and emit a second ALTER statement in order
         to add the constraint separately::
-    
+
             from alembic import op
             from sqlalchemy import Column, INTEGER, ForeignKey
 
             op.add_column('organization', 
                 Column('account_id', INTEGER, ForeignKey('accounts.id'))
-            )        
-    
+            )
+
         :param table_name: String name of the parent table.
         :param column: a :class:`sqlalchemy.schema.Column` object
          representing the new column.
-     
+
         """
 
         t = self._table(table_name, column)
@@ -260,11 +268,11 @@ class Operations(object):
 
     def drop_column(self, table_name, column_name, **kw):
         """Issue a "drop column" instruction using the current migration context.
-    
+
         e.g.::
-    
+
             drop_column('organization', 'account_id')
-    
+
         :param table_name: name of table
         :param column_name: name of column
         :param mssql_drop_check: Optional boolean.  When ``True``, on 
@@ -277,7 +285,7 @@ class Operations(object):
          drop the DEFAULT constraint on the column using a SQL-script-compatible
          block that selects into a @variable from sys.default_constraints,
          then exec's a separate DROP CONSTRAINT for that default.
-     
+
         """
 
         self.impl.drop_column(
@@ -292,7 +300,7 @@ class Operations(object):
         current migration context.
 
         e.g.::
-    
+
             from alembic import op
             op.create_foreign_key("fk_user_address", "address", "user", ["user_id"], ["id"])
 
@@ -303,7 +311,7 @@ class Operations(object):
         Any event listeners associated with this action will be fired 
         off normally.   The :class:`~sqlalchemy.schema.AddConstraint`
         construct is ultimately used to generate the ALTER statement.
-    
+
         :param name: Name of the foreign key constraint.  The name is necessary
          so that an ALTER statement can be emitted.  For setups that
          use an automated naming scheme such as that described at
@@ -319,7 +327,7 @@ class Operations(object):
          source table.
         :param remote_cols: a list of string column names in the
          remote table.
-    
+
         """
 
         self.impl.add_constraint(
@@ -331,7 +339,7 @@ class Operations(object):
         """Issue a "create unique constraint" instruction using the current migration context.
 
         e.g.::
-    
+
             from alembic import op
             op.create_unique_constraint("uq_user_name", "user", ["name"])
 
@@ -342,7 +350,7 @@ class Operations(object):
         Any event listeners associated with this action will be fired 
         off normally.   The :class:`~sqlalchemy.schema.AddConstraint`
         construct is ultimately used to generate the ALTER statement.
-    
+
         :param name: Name of the unique constraint.  The name is necessary
          so that an ALTER statement can be emitted.  For setups that
          use an automated naming scheme such as that described at
@@ -358,7 +366,7 @@ class Operations(object):
          issuing DDL for this constraint.
         :param initially: optional string. If set, emit INITIALLY <value> when issuing DDL
          for this constraint.
-    
+
         """
 
         self.impl.add_constraint(
@@ -368,18 +376,18 @@ class Operations(object):
 
     def create_check_constraint(self, name, source, condition, **kw):
         """Issue a "create check constraint" instruction using the current migration context.
-    
+
         e.g.::
-    
+
             from alembic import op
             from sqlalchemy.sql import column, func
-        
+
             op.create_check_constraint(
                 "ck_user_name_len",
                 "user", 
                 func.len(column('name')) > 5
             )
-    
+
         CHECK constraints are usually against a SQL expression, so ad-hoc
         table metadata is usually needed.   The function will convert the given 
         arguments into a :class:`sqlalchemy.schema.CheckConstraint` bound 
@@ -394,13 +402,13 @@ class Operations(object):
          with the table.
         :param source: String name of the source table.  Currently
          there is no support for dotted schema names.
-        :param condition: SQL expression that's the condition of the constraint.  
+        :param condition: SQL expression that's the condition of the constraint.
          Can be a string or SQLAlchemy expression language structure.
         :param deferrable: optional bool. If set, emit DEFERRABLE or NOT DEFERRABLE when
          issuing DDL for this constraint.
         :param initially: optional string. If set, emit INITIALLY <value> when issuing DDL
          for this constraint.
-    
+
         """
         self.impl.add_constraint(
             self._check_constraint(name, source, condition, **kw)
@@ -408,11 +416,11 @@ class Operations(object):
 
     def create_table(self, name, *columns, **kw):
         """Issue a "create table" instruction using the current migration context.
-    
+
         This directive receives an argument list similar to that of the 
         traditional :class:`sqlalchemy.schema.Table` construct, but without the
         metadata::
-        
+
             from sqlalchemy import INTEGER, VARCHAR, NVARCHAR, Column
             from alembic import op
 
@@ -432,7 +440,7 @@ class Operations(object):
          type will emit a CREATE TYPE within these events.
         :param \**kw: Other keyword arguments are passed to the underlying
          :class:`.Table` object created for the command.
-     
+
         """
         self.impl.create_table(
             self._table(name, *columns, **kw)
@@ -440,12 +448,12 @@ class Operations(object):
 
     def drop_table(self, name):
         """Issue a "drop table" instruction using the current migration context.
-    
-    
+
+
         e.g.::
-    
+
             drop_table("accounts")
-        
+
         """
         self.impl.drop_table(
             self._table(name)
@@ -453,9 +461,9 @@ class Operations(object):
 
     def create_index(self, name, tablename, *columns, **kw):
         """Issue a "create index" instruction using the current migration context.
-    
+
         e.g.::
-        
+
             from alembic import op
             op.create_index('ik_test', 't1', ['foo', 'bar'])
 
@@ -467,12 +475,12 @@ class Operations(object):
 
     def drop_index(self, name):
         """Issue a "drop index" instruction using the current migration context.
-    
-    
+
+
         e.g.::
-    
+
             drop_index("accounts")
-        
+
         """
         self.impl.drop_index(self._index(name, 'foo', []))
 
@@ -485,26 +493,26 @@ class Operations(object):
 
     def bulk_insert(self, table, rows):
         """Issue a "bulk insert" operation using the current migration context.
-    
+
         This provides a means of representing an INSERT of multiple rows
         which works equally well in the context of executing on a live 
         connection as well as that of generating a SQL script.   In the 
         case of a SQL script, the values are rendered inline into the 
         statement.
-    
+
         e.g.::
-    
+
             from datetime import date
             from sqlalchemy.sql import table, column
             from sqlalchemy import String, Integer, Date
-        
+
             # Create an ad-hoc table to use for the insert statement.
             accounts_table = table('account',
                 column('id', Integer),
                 column('name', String),
                 column('create_date', Date)
             )
-        
+
             bulk_insert(accounts_table,
                 [
                     {'id':1, 'name':'John Smith', 'create_date':date(2010, 10, 5)},
@@ -518,12 +526,12 @@ class Operations(object):
     def inline_literal(self, value, type_=None):
         """Produce an 'inline literal' expression, suitable for 
         using in an INSERT, UPDATE, or DELETE statement.
-    
+
         When using Alembic in "offline" mode, CRUD operations
         aren't compatible with SQLAlchemy's default behavior surrounding
         literal values,
         which is that they are converted into bound values and passed
-        separately into the ``execute()`` method of the DBAPI cursor.   
+        separately into the ``execute()`` method of the DBAPI cursor.
         An offline SQL
         script needs to have these rendered inline.  While it should
         always be noted that inline literal values are an **enormous**
@@ -535,7 +543,7 @@ class Operations(object):
 
         See :meth:`.execute` for an example usage of
         :meth:`.inline_literal`.
-    
+
         :param value: The value to render.  Strings, integers, and simple
          numerics should be supported.   Other types like boolean,
          dates, etc. may or may not be supported yet by various 
@@ -551,31 +559,31 @@ class Operations(object):
 
     def execute(self, sql):
         """Execute the given SQL using the current migration context.
-    
+
         In a SQL script context, the statement is emitted directly to the 
         output stream.   There is *no* return result, however, as this
         function is oriented towards generating a change script
         that can run in "offline" mode.  For full interaction
         with a connected database, use the "bind" available 
         from the context::
-    
+
             from alembic import op
             connection = op.get_bind()
-    
+
         Also note that any parameterized statement here *will not work*
         in offline mode - INSERT, UPDATE and DELETE statements which refer
         to literal values would need to render
         inline expressions.   For simple use cases, the :meth:`.inline_literal`
         function can be used for **rudimentary** quoting of string values.
         For "bulk" inserts, consider using :meth:`.bulk_insert`.
-    
+
         For example, to emit an UPDATE statement which is equally
         compatible with both online and offline mode::
-    
+
             from sqlalchemy.sql import table, column
             from sqlalchemy import String
             from alembic import op
-        
+
             account = table('account', 
                 column('name', String)
             )
@@ -584,7 +592,7 @@ class Operations(object):
                     where(account.c.name==op.inline_literal('account 1')).\\
                     values({'name':op.inline_literal('account 2')})
                     )
-    
+
         Note above we also used the SQLAlchemy :func:`sqlalchemy.sql.expression.table`
         and :func:`sqlalchemy.sql.expression.column` constructs to make a brief,
         ad-hoc table construct just for our UPDATE statement.  A full
@@ -593,9 +601,9 @@ class Operations(object):
         the definition of a table is self-contained within the migration script,
         rather than imported from a module that may break compatibility with
         older migrations.
-    
+
         :param sql: Any legal SQLAlchemy expression, including:
-    
+
         * a string
         * a :func:`sqlalchemy.sql.expression.text` construct.
         * a :func:`sqlalchemy.sql.expression.insert` construct.
@@ -604,19 +612,19 @@ class Operations(object):
         * Pretty much anything that's "executable" as described
           in :ref:`sqlexpression_toplevel`.
 
-    
+
         """
         self.migration_context.impl.execute(sql)
 
     def get_bind(self):
         """Return the current 'bind'.
-    
+
         Under normal circumstances, this is the 
         :class:`sqlalchemy.engine.Connection` currently being used
         to emit SQL to the database.
-    
+
         In a SQL script context, this value is ``None``. [TODO: verify this]
-    
+
         """
         return self.migration_context.impl.bind
 
index 1ad799399513c67889df3662fc27be72982a5123..dab204b6e30ebda4613bad87cde302d68a74f539 100644 (file)
Binary files a/docs/build/api_overview.png and b/docs/build/api_overview.png differ
index b363d526975f44ad2eea319c0fde908a19aab558..7c083e516414e7ecd661699a8e2b793090250a5a 100644 (file)
        <integer>5</integer>
        <key>GraphicsList</key>
        <array>
+               <dict>
+                       <key>Bounds</key>
+                       <string>{{319.25, 165}, {66, 12}}</string>
+                       <key>Class</key>
+                       <string>ShapedGraphic</string>
+                       <key>FitText</key>
+                       <string>YES</string>
+                       <key>Flow</key>
+                       <string>Resize</string>
+                       <key>ID</key>
+                       <integer>2054</integer>
+                       <key>Shape</key>
+                       <string>Rectangle</string>
+                       <key>Style</key>
+                       <dict>
+                               <key>shadow</key>
+                               <dict>
+                                       <key>Draws</key>
+                                       <string>NO</string>
+                               </dict>
+                               <key>stroke</key>
+                               <dict>
+                                       <key>Draws</key>
+                                       <string>NO</string>
+                               </dict>
+                       </dict>
+                       <key>Text</key>
+                       <dict>
+                               <key>Align</key>
+                               <integer>0</integer>
+                               <key>Text</key>
+                               <string>{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural
+
+\f0\fs20 \cf0 &lt;&lt;proxies&gt;&gt;}</string>
+                       </dict>
+                       <key>Wrap</key>
+                       <string>NO</string>
+               </dict>
+               <dict>
+                       <key>Bounds</key>
+                       <string>{{444, 216.633}, {66, 12}}</string>
+                       <key>Class</key>
+                       <string>ShapedGraphic</string>
+                       <key>FitText</key>
+                       <string>YES</string>
+                       <key>Flow</key>
+                       <string>Resize</string>
+                       <key>ID</key>
+                       <integer>2053</integer>
+                       <key>Shape</key>
+                       <string>Rectangle</string>
+                       <key>Style</key>
+                       <dict>
+                               <key>shadow</key>
+                               <dict>
+                                       <key>Draws</key>
+                                       <string>NO</string>
+                               </dict>
+                               <key>stroke</key>
+                               <dict>
+                                       <key>Draws</key>
+                                       <string>NO</string>
+                               </dict>
+                       </dict>
+                       <key>Text</key>
+                       <dict>
+                               <key>Align</key>
+                               <integer>0</integer>
+                               <key>Text</key>
+                               <string>{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf360
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural
+
+\f0\fs20 \cf0 &lt;&lt;proxies&gt;&gt;}</string>
+                       </dict>
+                       <key>Wrap</key>
+                       <string>NO</string>
+               </dict>
                <dict>
                        <key>Class</key>
                        <string>LineGraphic</string>
                        <key>Head</key>
                        <dict>
                                <key>ID</key>
-                               <integer>2042</integer>
+                               <integer>33</integer>
                        </dict>
                        <key>ID</key>
                        <integer>2046</integer>
                        <real>28.725006103515625</real>
                        <key>Points</key>
                        <array>
-                               <string>{304, 198.756}</string>
                                <string>{385.25, 157}</string>
+                               <string>{304, 191.818}</string>
                        </array>
                        <key>Style</key>
                        <dict>
                        <key>Tail</key>
                        <dict>
                                <key>ID</key>
-                               <integer>33</integer>
+                               <integer>2042</integer>
                        </dict>
                </dict>
                <dict>
                        <key>Head</key>
                        <dict>
                                <key>ID</key>
-                               <integer>2043</integer>
+                               <integer>38</integer>
                        </dict>
                        <key>ID</key>
                        <integer>2044</integer>
                        <real>52.850021362304688</real>
                        <key>Points</key>
                        <array>
-                               <string>{433.496, 294.6}</string>
                                <string>{454.25, 177}</string>
+                               <string>{442.638, 294.6}</string>
                        </array>
                        <key>Style</key>
                        <dict>
                        <key>Tail</key>
                        <dict>
                                <key>ID</key>
-                               <integer>38</integer>
+                               <integer>2043</integer>
                        </dict>
                </dict>
                <dict>
@@ -1095,7 +1177,7 @@ is_offline_mode()}</string>
                </dict>
        </array>
        <key>ModificationDate</key>
-       <string>2012-01-24 17:14:19 -0500</string>
+       <string>2012-01-24 17:59:01 -0500</string>
        <key>Modifier</key>
        <string>classic</string>
        <key>NotesVisible</key>
@@ -1167,7 +1249,7 @@ is_offline_mode()}</string>
                <key>FitInWindow</key>
                <false/>
                <key>Frame</key>
-               <string>{{335, 186}, {760, 817}}</string>
+               <string>{{335, 211}, {760, 817}}</string>
                <key>ShowRuler</key>
                <false/>
                <key>ShowStatusBar</key>
index 0460f350ee7d7a198316c36e820833d52ba23644..c3bcaaee3ff4369d098d7f9b803884a6dc844900 100644 (file)
@@ -56,18 +56,25 @@ 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`` and ``alembic.context`` names are no longer Python modules,
-  and are instead objects placed at those names when migrations run.   This 
-  means an env.py script or migration script that tries to import from 
-  the object will fail, such as ``from alembic.op import create_table``.
-  The imports used should now be of the form ``from alembic import context``
-  and ``from alembic import op``.   The various methods associated with the
-  context and ops should be invoked from those names now, such as ``op.create_table()``.
-  The included files and the tutorial in 0.1 already did things this way,
-  though the examples for each ``op`` docstring did not.   Hopefully most users
-  stuck with the tutorial convention, where the usage examples will
-  still work without change.
-
+* 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
index e9d5bf0c9272e706f6206f6846a07b40961afc7c..4322bfb57f8335a8b132af062d6084f034dd31a7 100644 (file)
@@ -14,6 +14,9 @@ All directives exist as methods on a class called :class:`.Operations`.
 When migration scripts are run, this object is made available
 to the script via the ``alembic.op`` datamember, which is
 a *proxy* to an actual instance of :class:`.Operations`.
+Currently, ``alembic.op`` is a real Python module, populated
+with individual proxies for each method on :class:`.Operations`,
+so symbols can be imported safely from the ``alembic.op`` namespace.
 
 A key design philosophy to the :mod:`alembic.operations` methods is that
 to the greatest degree possible, they internally generate the