From: Mike Bayer Date: Tue, 24 Jan 2012 23:10:34 +0000 (-0500) Subject: - tried it out in my work project, and realized the "op" and "context" namespaces X-Git-Tag: rel_0_2_0~11 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8175face4f37c8119c5674958135e2e51a177c24;p=thirdparty%2Fsqlalchemy%2Falembic.git - tried it out in my work project, and realized the "op" and "context" namespaces 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 --- diff --git a/alembic/__init__.py b/alembic/__init__.py index e561d0d9..09d91dfa 100644 --- a/alembic/__init__.py +++ b/alembic/__init__.py @@ -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() + diff --git a/alembic/environment.py b/alembic/environment.py index d562add6..f61c9c7b 100644 --- a/alembic/environment.py +++ b/alembic/environment.py @@ -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 index 00000000..8a5e0fa0 --- /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 18b8e692..f3e6708c 100644 --- a/alembic/operations.py +++ b/alembic/operations.py @@ -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 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 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 diff --git a/docs/build/api_overview.png b/docs/build/api_overview.png index 1ad79939..dab204b6 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 b363d526..7c083e51 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 0460f350..c3bcaaee 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 e9d5bf0c..4322bfb5 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