diff --git a/alembic/__init__.py b/alembic/__init__.py index e561d0d..09d91df 100644 --- a/alembic/__init__.py +++ b/alembic/__init__.py @@ -5,8 +5,11 @@ 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() + diff --git a/alembic/environment.py b/alembic/environment.py index d562add..f61c9c7 100644 --- a/alembic/environment.py +++ b/alembic/environment.py @@ -46,11 +46,11 @@ def __enter__(self): 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 index 0000000..8a5e0fa --- /dev/null +++ b/alembic/op.py @@ -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 diff --git a/alembic/operations.py b/alembic/operations.py index 18b8e69..f3e6708 100644 --- a/alembic/operations.py +++ b/alembic/operations.py @@ -9,10 +9,10 @@ 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 @@ def _ensure_table_for_fk(self, metadata, fk): 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 @@ def alter_column(self, table_name, column_name, ): """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 @@ def alter_column(self, table_name, column_name, 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 @@ def alter_column(self, table_name, column_name, :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 @@ def alter_column(self, table_name, column_name, 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 @@ def add_column(self, table_name, column): 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 @@ def add_column(self, table_name, column): 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 @@ def drop_column(self, table_name, column_name, **kw): 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 @@ def create_foreign_key(self, name, source, referent, local_cols, remote_cols): 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 @@ def create_foreign_key(self, name, source, referent, local_cols, remote_cols): 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 @@ def create_foreign_key(self, name, source, referent, local_cols, remote_cols): source table. :param remote_cols: a list of string column names in the remote table. - + """ self.impl.add_constraint( @@ -331,7 +339,7 @@ def create_unique_constraint(self, name, source, local_cols, **kw): """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 @@ def create_unique_constraint(self, name, source, local_cols, **kw): 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 @@ def create_unique_constraint(self, name, source, local_cols, **kw): issuing DDL for this constraint. :param initially: optional string. If set, emit INITIALLY when issuing DDL for this constraint. - + """ self.impl.add_constraint( @@ -368,18 +376,18 @@ def create_unique_constraint(self, name, source, local_cols, **kw): 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 @@ def create_check_constraint(self, name, source, condition, **kw): 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 when issuing DDL for this constraint. - + """ self.impl.add_constraint( self._check_constraint(name, source, condition, **kw) @@ -408,11 +416,11 @@ def create_check_constraint(self, name, source, condition, **kw): 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 @@ def create_table(self, name, *columns, **kw): 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 @@ def create_table(self, name, *columns, **kw): 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 @@ def drop_table(self, name): 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 @@ def create_index(self, name, tablename, *columns, **kw): 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 @@ def drop_constraint(self, name, tablename): 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 @@ def bulk_insert(self, table, rows): 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 @@ def inline_literal(self, value, type_=None): 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 @@ def inline_literal(self, value, type_=None): 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 @@ def execute(self, sql): 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 @@ def execute(self, sql): 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 @@ def execute(self, sql): * 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 diff --git a/docs/build/api_overview.png b/docs/build/api_overview.png index 1ad7993..dab204b 100644 Binary files a/docs/build/api_overview.png and b/docs/build/api_overview.png differ diff --git a/docs/build/assets/api_overview.graffle b/docs/build/assets/api_overview.graffle index b363d52..7c083e5 100644 --- a/docs/build/assets/api_overview.graffle +++ b/docs/build/assets/api_overview.graffle @@ -29,6 +29,88 @@ 5 GraphicsList + + Bounds + {{319.25, 165}, {66, 12}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 2054 + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Text + {\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 <<proxies>>} + + Wrap + NO + + + Bounds + {{444, 216.633}, {66, 12}} + Class + ShapedGraphic + FitText + YES + Flow + Resize + ID + 2053 + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Text + {\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 <<proxies>>} + + Wrap + NO + Class LineGraphic @@ -159,7 +241,7 @@ Head ID - 2042 + 33 ID 2046 @@ -169,8 +251,8 @@ 28.725006103515625 Points - {304, 198.756} {385.25, 157} + {304, 191.818} Style @@ -189,7 +271,7 @@ Tail ID - 33 + 2042 @@ -198,7 +280,7 @@ Head ID - 2043 + 38 ID 2044 @@ -208,8 +290,8 @@ 52.850021362304688 Points - {433.496, 294.6} {454.25, 177} + {442.638, 294.6} Style @@ -228,7 +310,7 @@ Tail ID - 38 + 2043 @@ -1095,7 +1177,7 @@ is_offline_mode()} ModificationDate - 2012-01-24 17:14:19 -0500 + 2012-01-24 17:59:01 -0500 Modifier classic NotesVisible @@ -1167,7 +1249,7 @@ is_offline_mode()} FitInWindow Frame - {{335, 186}, {760, 817}} + {{335, 211}, {760, 817}} ShowRuler ShowStatusBar diff --git a/docs/build/front.rst b/docs/build/front.rst index 0460f35..c3bcaae 100644 --- a/docs/build/front.rst +++ b/docs/build/front.rst @@ -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 diff --git a/docs/build/ops.rst b/docs/build/ops.rst index e9d5bf0..4322bfb 100644 --- a/docs/build/ops.rst +++ b/docs/build/ops.rst @@ -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