summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2012-04-05 13:33:37 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2012-04-05 13:33:37 -0400
commit8332e56a18e30ebc5c74bf782f6ad42d14ea9814 (patch)
tree77376be5ec647966d29deee768a4eddb90cca94e
parent67fda40a43c79f532f5d3c4f041e2203a126fffd (diff)
downloadalembic-8332e56a18e30ebc5c74bf782f6ad42d14ea9814.tar.gz
- move to 0.3 as we are changing APIrel_0_3_0
- [general] The focus of 0.3 is to clean up and more fully document the public API of Alembic, including better accessors on the MigrationContext and ScriptDirectory objects. Methods that are not considered to be public on these objects have been underscored, and methods which should be public have been cleaned up and documented, including: MigrationContext.get_current_revision() ScriptDirectory.iterate_revisions() ScriptDirectory.get_current_head() ScriptDirectory.get_heads() ScriptDirectory.get_base() ScriptDirectory.generate_revision() - [feature] Added a bit of autogenerate to the public API in the form of the function alembic.autogenerate.compare_metadata.
-rw-r--r--CHANGES25
-rw-r--r--alembic/__init__.py2
-rw-r--r--alembic/autogenerate.py113
-rw-r--r--alembic/command.py41
-rw-r--r--alembic/config.py1
-rw-r--r--alembic/environment.py54
-rw-r--r--alembic/migration.py95
-rw-r--r--alembic/script.py195
-rw-r--r--docs/build/api.rst87
-rw-r--r--docs/build/conf.py2
-rw-r--r--docs/build/front.rst30
-rw-r--r--tests/__init__.py12
-rw-r--r--tests/test_autogenerate.py14
-rw-r--r--tests/test_postgresql.py2
-rw-r--r--tests/test_revision_create.py23
-rw-r--r--tests/test_revision_paths.py18
-rw-r--r--tests/test_versioning.py12
17 files changed, 581 insertions, 145 deletions
diff --git a/CHANGES b/CHANGES
index 7950a13..001bd6a 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,24 @@
+0.3.0
+=====
+- [general] The focus of 0.3 is to clean up
+ and more fully document the public API of Alembic,
+ including better accessors on the MigrationContext
+ and ScriptDirectory objects. Methods that are
+ not considered to be public on these objects have
+ been underscored, and methods which should be public
+ have been cleaned up and documented, including:
+
+ MigrationContext.get_current_revision()
+ ScriptDirectory.iterate_revisions()
+ ScriptDirectory.get_current_head()
+ ScriptDirectory.get_heads()
+ ScriptDirectory.get_base()
+ ScriptDirectory.generate_revision()
+
+- [feature] Added a bit of autogenerate to the
+ public API in the form of the function
+ alembic.autogenerate.compare_metadata.
+
0.2.2
=====
- [feature] Informative error message when op.XYZ
@@ -196,7 +217,7 @@
into a new schema. For dev environments, the
dev installer should be building the whole DB from
scratch. Or just use Postgresql, which is a much
- better database for non-trivial schemas.
+ better database for non-trivial schemas.
Requests for full ALTER support on SQLite should be
reported to SQLite's bug tracker at
http://www.sqlite.org/src/wiki?name=Bug+Reports,
@@ -258,7 +279,7 @@
by key, etc. for full support here.
- Support for tables in remote schemas,
- i.e. "schemaname.tablename", is very poor.
+ i.e. "schemaname.tablename", is very poor.
Missing "schema" behaviors should be
reported as tickets, though in the author's
experience, migrations typically proceed only
diff --git a/alembic/__init__.py b/alembic/__init__.py
index 7c50518..c1c253c 100644
--- a/alembic/__init__.py
+++ b/alembic/__init__.py
@@ -1,6 +1,6 @@
from os import path
-__version__ = '0.2.2'
+__version__ = '0.3.0'
package_dir = path.abspath(path.dirname(__file__))
diff --git a/alembic/autogenerate.py b/alembic/autogenerate.py
index ba0ecd9..5fa856c 100644
--- a/alembic/autogenerate.py
+++ b/alembic/autogenerate.py
@@ -11,10 +11,95 @@ import logging
log = logging.getLogger(__name__)
###################################################
-# top level
+# public
+def compare_metadata(context, metadata):
+ """Compare a database schema to that given in a :class:`~sqlalchemy.schema.MetaData`
+ instance.
+
+ The database connection is presented in the context
+ of a :class:`.MigrationContext` object, which
+ provides database connectivity as well as optional
+ comparison functions to use for datatypes and
+ server defaults - see the "autogenerate" arguments
+ at :meth:`.EnvironmentContext.configure`
+ for details on these.
+
+ The return format is a list of "diff" directives,
+ each representing individual differences::
+
+ from alembic.migration import MigrationContext
+ from alembic.autogenerate import compare_metadata
+ from sqlalchemy.schema import SchemaItem
+ from sqlalchemy.types import TypeEngine
+ from sqlalchemy import (create_engine, MetaData, Column,
+ Integer, String, Table)
+ import pprint
+
+ engine = create_engine("sqlite://")
+
+ engine.execute('''
+ create table foo (
+ id integer not null primary key,
+ old_data varchar,
+ x integer
+ )''')
+
+ engine.execute('''
+ create table bar (
+ data varchar
+ )''')
+
+ metadata = MetaData()
+ Table('foo', metadata,
+ Column('id', Integer, primary_key=True),
+ Column('data', Integer),
+ Column('x', Integer, nullable=False)
+ )
+ Table('bat', metadata,
+ Column('info', String)
+ )
+
+ mc = MigrationContext.configure(engine.connect())
+
+ diff = compare_metadata(mc, metadata)
+ pprint.pprint(diff, indent=2, width=20)
+
+ Output::
+
+ [ ( 'add_table',
+ Table('bat', MetaData(bind=None), Column('info', String(), table=<bat>), schema=None)),
+ ( 'remove_table',
+ Table(u'bar', MetaData(bind=None), Column(u'data', VARCHAR(), table=<bar>), schema=None)),
+ ( 'add_column',
+ 'foo',
+ Column('data', Integer(), table=<foo>)),
+ ( 'remove_column',
+ 'foo',
+ Column(u'old_data', VARCHAR(), table=None)),
+ [ ( 'modify_nullable',
+ 'foo',
+ u'x',
+ { 'existing_server_default': None,
+ 'existing_type': INTEGER()},
+ True,
+ False)]]
+
+
+ :param context: a :class:`.MigrationContext`
+ instance.
+ :param metadata: a :class:`~sqlalchemy.schema.MetaData`
+ instance.
+
+ """
+ autogen_context, connection = _autogen_context(context, None)
+ diffs = []
+ _produce_net_changes(connection, metadata, diffs, autogen_context)
+ return diffs
+###################################################
+# top level
-def produce_migration_diffs(context, template_args, imports):
+def _produce_migration_diffs(context, template_args, imports):
opts = context.opts
metadata = opts['target_metadata']
if metadata is None:
@@ -24,15 +109,9 @@ def produce_migration_diffs(context, template_args, imports):
"a MetaData object to the context." % (
context.script.env_py_location
))
- connection = context.bind
+ autogen_context, connection = _autogen_context(context, imports)
+
diffs = []
- autogen_context = {
- 'imports':imports,
- 'connection':connection,
- 'dialect':connection.dialect,
- 'context':context,
- 'opts':opts
- }
_produce_net_changes(connection, metadata, diffs, autogen_context)
template_args[opts['upgrade_token']] = \
_indent(_produce_upgrade_commands(diffs, autogen_context))
@@ -40,6 +119,16 @@ def produce_migration_diffs(context, template_args, imports):
_indent(_produce_downgrade_commands(diffs, autogen_context))
template_args['imports'] = "\n".join(sorted(imports))
+def _autogen_context(context, imports):
+ opts = context.opts
+ connection = context.bind
+ return {
+ 'imports':imports,
+ 'connection':connection,
+ 'dialect':connection.dialect,
+ 'context':context,
+ 'opts':opts
+ }, connection
def _indent(text):
text = "### commands auto generated by Alembic - please adjust! ###\n" + text
@@ -178,7 +267,7 @@ def _compare_type(tname, cname, conn_col,
log.info("Column '%s.%s' has no type within the model; can't compare" % (tname, cname))
return
- isdiff = autogen_context['context'].compare_type(conn_col, metadata_col)
+ isdiff = autogen_context['context']._compare_type(conn_col, metadata_col)
if isdiff:
@@ -203,7 +292,7 @@ def _compare_server_default(tname, cname, conn_col, metadata_col,
if conn_col_default is None and metadata_default is None:
return False
rendered_metadata_default = _render_server_default(metadata_default, autogen_context)
- isdiff = autogen_context['context'].compare_server_default(
+ isdiff = autogen_context['context']._compare_server_default(
conn_col, metadata_col,
rendered_metadata_default
)
diff --git a/alembic/command.py b/alembic/command.py
index 18b421c..ed7b830 100644
--- a/alembic/command.py
+++ b/alembic/command.py
@@ -1,7 +1,7 @@
from alembic.script import ScriptDirectory
-from alembic import util, ddl, autogenerate as autogen, environment
+from alembic.environment import EnvironmentContext
+from alembic import util, ddl, autogenerate as autogen
import os
-import functools
def list_templates(config):
"""List available templates"""
@@ -44,14 +44,14 @@ def init(config, directory, template='generic'):
if os.access(config_file, os.F_OK):
util.msg("File %s already exists, skipping" % config_file)
else:
- script.generate_template(
+ script._generate_template(
os.path.join(template_dir, file_),
config_file,
script_location=directory
)
else:
output_file = os.path.join(directory, file_)
- script.copy_file(
+ script._copy_file(
os.path.join(template_dir, file_),
output_file
)
@@ -68,18 +68,18 @@ def revision(config, message=None, autogenerate=False):
if autogenerate:
util.requires_07("autogenerate")
def retrieve_migrations(rev, context):
- if script._get_rev(rev) is not script._get_rev("head"):
+ if script.get_revision(rev) is not script.get_revision("head"):
raise util.CommandError("Target database is not up to date.")
- autogen.produce_migration_diffs(context, template_args, imports)
+ autogen._produce_migration_diffs(context, template_args, imports)
return []
- with environment.configure(
+ with EnvironmentContext(
config,
script,
fn = retrieve_migrations
):
script.run_env()
- script.generate_rev(util.rev_id(), message, **template_args)
+ script.generate_revision(util.rev_id(), message, **template_args)
def upgrade(config, revision, sql=False, tag=None):
@@ -92,10 +92,14 @@ def upgrade(config, revision, sql=False, tag=None):
if not sql:
raise util.CommandError("Range revision not allowed")
starting_rev, revision = revision.split(':', 2)
- with environment.configure(
+
+ def upgrade(rev, context):
+ return script._upgrade_revs(revision, rev)
+
+ with EnvironmentContext(
config,
script,
- fn = functools.partial(script.upgrade_from, revision),
+ fn = upgrade,
as_sql = sql,
starting_rev = starting_rev,
destination_rev = revision,
@@ -114,10 +118,13 @@ def downgrade(config, revision, sql=False, tag=None):
raise util.CommandError("Range revision not allowed")
starting_rev, revision = revision.split(':', 2)
- with environment.configure(
+ def downgrade(rev, context):
+ return script._downgrade_revs(revision, rev)
+
+ with EnvironmentContext(
config,
script,
- fn = functools.partial(script.downgrade_to, revision),
+ fn = downgrade,
as_sql = sql,
starting_rev = starting_rev,
destination_rev = revision,
@@ -143,7 +150,7 @@ def branches(config):
for rev in sc.nextrev:
print "%s -> %s" % (
" " * len(str(sc.down_revision)),
- script._get_rev(rev)
+ script.get_revision(rev)
)
def current(config):
@@ -154,10 +161,10 @@ def current(config):
print "Current revision for %s: %s" % (
util.obfuscate_url_pw(
context.connection.engine.url),
- script._get_rev(rev))
+ script.get_revision(rev))
return []
- with environment.configure(
+ with EnvironmentContext(
config,
script,
fn = display_version
@@ -174,12 +181,12 @@ def stamp(config, revision, sql=False, tag=None):
current = False
else:
current = context._current_rev()
- dest = script._get_rev(revision)
+ dest = script.get_revision(revision)
if dest is not None:
dest = dest.revision
context._update_current_rev(current, dest)
return []
- with environment.configure(
+ with EnvironmentContext(
config,
script,
fn = do_stamp,
diff --git a/alembic/config.py b/alembic/config.py
index 998bb85..92e0921 100644
--- a/alembic/config.py
+++ b/alembic/config.py
@@ -32,6 +32,7 @@ class Config(object):
from alembic.config import Config
alembic_cfg = Config()
+ alembic_cfg.set_main_option("script_location", "myapp:migrations")
alembic_cfg.set_main_option("url", "postgresql://foo/bar")
alembic_cfg.set_section_option("mysection", "foo", "bar")
diff --git a/alembic/environment.py b/alembic/environment.py
index 231aceb..52c353a 100644
--- a/alembic/environment.py
+++ b/alembic/environment.py
@@ -13,6 +13,50 @@ class EnvironmentContext(object):
:class:`.EnvironmentContext` is available via the
``alembic.context`` datamember.
+ :class:`.EnvironmentContext` is also a Python context
+ manager, that is, is intended to be used using the
+ ``with:`` statement. A typical use of :class:`.EnvironmentContext`::
+
+ from alembic.config import Config
+ from alembic.script import ScriptDirectory
+
+ config = Config()
+ config.set_main_option("script_location", "myapp:migrations")
+ script = ScriptDirectory.from_config(config)
+
+ def my_function(rev, context):
+ '''do something with revision "rev", which
+ will be the current database revision,
+ and "context", which is the MigrationContext
+ that the env.py will create'''
+
+ with EnvironmentContext(
+ config,
+ script,
+ fn = my_function,
+ as_sql = False,
+ starting_rev = 'base',
+ destination_rev = 'head',
+ tag = "sometag"
+ ):
+ script.run_env()
+
+ The above script will invoke the ``env.py`` script
+ within the migration environment. If and when ``env.py``
+ calls :meth:`.MigrationContext.run_migrations`, the
+ ``my_function()`` function above will be called
+ by the :class:`.MigrationContext`, given the context
+ itself as well as the current revision in the database.
+
+ .. note::
+
+ For most API usages other than full blown
+ invocation of migration scripts, the :class:`.MigrationContext`
+ and :class:`.ScriptDirectory` objects can be created and
+ used directly. The :class:`.EnvironmentContext` object
+ is *only* needed when you need to actually invoke the
+ ``env.py`` module present in the migration environment.
+
"""
_migration_context = None
@@ -31,6 +75,15 @@ class EnvironmentContext(object):
"""
def __init__(self, config, script, **kw):
+ """Construct a new :class:`.EnvironmentContext`.
+
+ :param config: a :class:`.Config` instance.
+ :param script: a :class:`.ScriptDirectory` instance.
+ :param \**kw: keyword options that will be ultimately
+ passed along to the :class:`.MigrationContext` when
+ :meth:`.EnvironmentContext.configure` is called.
+
+ """
self.config = config
self.script = script
self.context_opts = kw
@@ -495,4 +548,3 @@ class EnvironmentContext(object):
def get_impl(self):
return self.get_context().impl
-configure = EnvironmentContext
diff --git a/alembic/migration.py b/alembic/migration.py
index d610b3b..80e53b2 100644
--- a/alembic/migration.py
+++ b/alembic/migration.py
@@ -15,31 +15,48 @@ _version = Table('alembic_version', _meta,
)
class MigrationContext(object):
- """Represent the state made available to a migration script,
- or otherwise a series of migration operations.
+ """Represent the database state made available to a migration
+ script.
- Mediates the relationship between an ``env.py`` environment script,
- a :class:`.ScriptDirectory` instance, and a :class:`.DefaultImpl` instance.
-
- The :class:`.MigrationContext` that's established for a
- duration of a migration command is available via the
+ :class:`.MigrationContext` is the front end to an actual
+ database connection, or alternatively a string output
+ stream given a particular database dialect,
+ from an Alembic perspective.
+
+ When inside the ``env.py`` script, the :class:`.MigrationContext`
+ is available via the
:meth:`.EnvironmentContext.get_context` method,
which is available at ``alembic.context``::
+ # from within env.py script
from alembic import context
migration_context = context.get_context()
- A :class:`.MigrationContext` can be created programmatically
- for usage outside of the usual Alembic migrations flow,
- using the :meth:`.MigrationContext.configure` method::
+ For usage outside of an ``env.py`` script, such as for
+ utility routines that want to check the current version
+ in the database, the :meth:`.MigrationContext.configure`
+ method to create new :class:`.MigrationContext` objects.
+ For example, to get at the current revision in the
+ database using :meth:`.MigrationContext.get_current_revision`::
- conn = myengine.connect()
- ctx = MigrationContext.configure(conn)
+ # in any application, outside of an env.py script
+ from alembic.migration import MigrationContext
+ from sqlalchemy import create_engine
+
+ engine = create_engine("postgresql://mydatabase")
+ conn = engine.connect()
+
+ context = MigrationContext.configure(conn)
+ current_rev = context.get_current_revision()
- The above context can then be used to produce
+ The above context can also be used to produce
Alembic migration operations with an :class:`.Operations`
- instance.
-
+ instance::
+
+ # in any application, outside of the normal Alembic environment
+ from alembic.operations import Operations
+ op = Operations(context)
+ op.alter_column("mytable", "somecolumn", nullable=True)
"""
def __init__(self, dialect, connection, opts):
@@ -119,7 +136,15 @@ class MigrationContext(object):
return MigrationContext(dialect, connection, opts)
- def _current_rev(self):
+ def get_current_revision(self):
+ """Return the current revision, usually that which is present
+ in the ``alembic_version`` table in the database.
+
+ If this :class:`.MigrationContext` was configured in "offline"
+ mode, that is with ``as_sql=True``, the ``starting_rev``
+ parameter is returned instead, if any.
+
+ """
if self.as_sql:
return self._start_from_rev
else:
@@ -130,6 +155,9 @@ class MigrationContext(object):
_version.create(self.connection, checkfirst=True)
return self.connection.scalar(_version.select())
+ _current_rev = get_current_revision
+ """The 0.2 method name, for backwards compat."""
+
def _update_current_rev(self, old, new):
if old == new:
return
@@ -145,11 +173,30 @@ class MigrationContext(object):
)
def run_migrations(self, **kw):
-
+ """Run the migration scripts established for this :class:`.MigrationContext`,
+ if any.
+
+ The commands in :mod:`alembic.command` will set up a function
+ that is ultimately passed to the :class:`.MigrationContext`
+ as the ``fn`` argument. This function represents the "work"
+ that will be done when :meth:`.MigrationContext.run_migrations`
+ is called, typically from within the ``env.py`` script of the
+ migration environment. The "work function" then provides an iterable
+ of version callables and other version information which
+ in the case of the ``upgrade`` or ``downgrade`` commands are the
+ list of version scripts to invoke. Other commands yield nothing,
+ in the case that a command wants to run some other operation
+ against the database such as the ``current`` or ``stamp`` commands.
+
+ :param \**kw: keyword arguments here will be passed to each
+ migration callable, that is the ``upgrade()`` or ``downgrade()``
+ method within revision scripts.
+
+ """
current_rev = rev = False
self.impl.start_migrations()
for change, prev_rev, rev in self._migrations_fn(
- self._current_rev(),
+ self.get_current_revision(),
self):
if current_rev is False:
current_rev = prev_rev
@@ -174,6 +221,14 @@ class MigrationContext(object):
_version.drop(self.connection)
def execute(self, sql):
+ """Execute a SQL construct or string statement.
+
+ The underlying execution mechanics are used, that is
+ if this is "offline mode" the SQL is written to the
+ output buffer, otherwise the SQL is emitted on
+ the current SQLAlchemy connection.
+
+ """
self.impl._exec(sql)
def _stdout_connection(self, connection):
@@ -203,7 +258,7 @@ class MigrationContext(object):
"""
return self.connection
- def compare_type(self, inspector_column, metadata_column):
+ def _compare_type(self, inspector_column, metadata_column):
if self._user_compare_type is False:
return False
@@ -222,7 +277,7 @@ class MigrationContext(object):
inspector_column,
metadata_column)
- def compare_server_default(self, inspector_column,
+ def _compare_server_default(self, inspector_column,
metadata_column,
rendered_metadata_default):
diff --git a/alembic/script.py b/alembic/script.py
index 952b572..bef7dfb 100644
--- a/alembic/script.py
+++ b/alembic/script.py
@@ -15,7 +15,22 @@ _default_file_template = "%(rev)s_%(slug)s"
class ScriptDirectory(object):
"""Provides operations upon an Alembic script directory.
-
+
+ This object is useful to get information as to current revisions,
+ most notably being able to get at the "head" revision, for schemes
+ that want to test if the current revision in the database is the most
+ recent::
+
+ from alembic.script import ScriptDirectory
+ from alembic.config import Config
+ config = Config()
+ config.set_main_option("script_location", "myapp:migrations")
+ script = ScriptDirectory.from_config(config)
+
+ head_revision = script.get_current_head()
+
+
+
"""
def __init__(self, dir, file_template=_default_file_template):
self.dir = dir
@@ -29,6 +44,13 @@ class ScriptDirectory(object):
@classmethod
def from_config(cls, config):
+ """Produce a new :class:`.ScriptDirectory` given a :class:`.Config`
+ instance.
+
+ The :class:`.Config` need only have the ``script_location`` key
+ present.
+
+ """
return ScriptDirectory(
util.coerce_resource_to_filename(
config.get_main_option('script_location')
@@ -45,23 +67,25 @@ class ScriptDirectory(object):
with leaf nodes being heads.
"""
- heads = set(self._get_heads())
- base = self._get_rev("base")
+ heads = set(self.get_heads())
+ base = self.get_revision("base")
while heads:
todo = set(heads)
heads = set()
for head in todo:
if head in heads:
break
- for sc in self._revs(head, base):
+ for sc in self.iterate_revisions(head, base):
if sc.is_branch_point and sc.revision not in todo:
heads.add(sc.revision)
break
else:
yield sc
- def _get_rev(self, id_):
- id_ = self._as_rev_number(id_)
+ def get_revision(self, id_):
+ """Return the :class:`.Script` instance with the given rev id."""
+
+ id_ = self.as_revision_number(id_)
try:
return self._revision_map[id_]
except KeyError:
@@ -80,16 +104,34 @@ class ScriptDirectory(object):
else:
return self._revision_map[revs[0]]
- def _as_rev_number(self, id_):
+ _get_rev = get_revision
+
+ def as_revision_number(self, id_):
+ """Convert a symbolic revision, i.e. 'head' or 'base', into
+ an actual revision number."""
+
if id_ == 'head':
- id_ = self._current_head()
+ id_ = self.get_current_head()
elif id_ == 'base':
id_ = None
return id_
- def _revs(self, upper, lower):
- lower = self._get_rev(lower)
- upper = self._get_rev(upper)
+ _as_rev_number = as_revision_number
+
+ def iterate_revisions(self, upper, lower):
+ """Iterate through script revisions, starting at the given
+ upper revision identifier and ending at the lower.
+
+ The traversal uses strictly the `down_revision`
+ marker inside each migration script, so
+ it is a requirement that upper >= lower,
+ else you'll get nothing back.
+
+ The iterator yields :class:`.Script` objects.
+
+ """
+ lower = self.get_revision(lower)
+ upper = self.get_revision(upper)
script = upper
while script != lower:
yield script
@@ -99,15 +141,15 @@ class ScriptDirectory(object):
raise util.CommandError(
"Couldn't find revision %s" % downrev)
- def upgrade_from(self, destination, current_rev, context):
- revs = self._revs(destination, current_rev)
+ def _upgrade_revs(self, destination, current_rev):
+ revs = self.iterate_revisions(destination, current_rev)
return [
(script.module.upgrade, script.down_revision, script.revision)
for script in reversed(list(revs))
]
- def downgrade_to(self, destination, current_rev, context):
- revs = self._revs(current_rev, destination)
+ def _downgrade_revs(self, destination, current_rev):
+ revs = self.iterate_revisions(current_rev, destination)
return [
(script.module.downgrade, script.revision, script.down_revision)
for script in revs
@@ -115,12 +157,12 @@ class ScriptDirectory(object):
def run_env(self):
"""Run the script environment.
-
+
This basically runs the ``env.py`` script present
in the migration environment. It is called exclusively
by the command functions in :mod:`alembic.command`.
-
-
+
+
"""
util.load_python_file(self.dir, 'env.py')
@@ -132,7 +174,7 @@ class ScriptDirectory(object):
def _revision_map(self):
map_ = {}
for file_ in os.listdir(self.versions):
- script = Script.from_filename(self.versions, file_)
+ script = Script._from_filename(self.versions, file_)
if script is None:
continue
if script.revision in map_:
@@ -158,8 +200,16 @@ class ScriptDirectory(object):
)
return os.path.join(self.versions, filename)
- def _current_head(self):
- current_heads = self._get_heads()
+ def get_current_head(self):
+ """Return the current head revision.
+
+ If the script directory has multiple heads
+ due to branching, an error is raised.
+
+ Returns a string revision number.
+
+ """
+ current_heads = self.get_heads()
if len(current_heads) > 1:
raise util.CommandError("Only a single head supported so far...")
if current_heads:
@@ -167,22 +217,45 @@ class ScriptDirectory(object):
else:
return None
- def _get_heads(self):
+ _current_head = get_current_head
+ """the 0.2 name, for backwards compat."""
+
+ def get_heads(self):
+ """Return all "head" revisions as strings.
+
+ Returns a list of string revision numbers.
+
+ This is normally a list of length one,
+ unless branches are present. The
+ :meth:`.ScriptDirectory.get_current_head()` method
+ can be used normally when a script directory
+ has only one head.
+
+ """
heads = []
for script in self._revision_map.values():
if script and script.is_head:
heads.append(script.revision)
return heads
- def _get_origin(self):
+ def get_base(self):
+ """Return the "base" revision as a string.
+
+ This is the revision number of the script that
+ has a ``down_revision`` of None.
+
+ Behavior is not defined if more than one script
+ has a ``down_revision`` of None.
+
+ """
for script in self._revision_map.values():
if script.down_revision is None \
and script.revision in self._revision_map:
- return script
+ return script.revision
else:
return None
- def generate_template(self, src, dest, **kw):
+ def _generate_template(self, src, dest, **kw):
util.status("Generating %s" % os.path.abspath(dest),
util.template_to_file,
src,
@@ -190,15 +263,33 @@ class ScriptDirectory(object):
**kw
)
- def copy_file(self, src, dest):
+ def _copy_file(self, src, dest):
util.status("Generating %s" % os.path.abspath(dest),
shutil.copy,
src, dest)
- def generate_rev(self, revid, message, refresh=False, **kw):
- current_head = self._current_head()
+ def generate_revision(self, revid, message, refresh=False, **kw):
+ """Generate a new revision file.
+
+ This runs the ``script.py.mako`` template, given
+ template arguments, and creates a new file.
+
+ :param revid: String revision id. Typically this
+ comes from ``alembic.util.rev_id()``.
+ :param message: the revision message, the one passed
+ by the -m argument to the ``revision`` command.
+ :param refresh: when True, the in-memory state of this
+ :class:`.ScriptDirectory` will be updated with a new
+ :class:`.Script` instance representing the new revision;
+ the :class:`.Script` instance is returned.
+ If False, the file is created but the state of the
+ :class:`.ScriptDirectory` is unmodified; ``None``
+ is returned.
+
+ """
+ current_head = self.get_current_head()
path = self._rev_path(revid, message)
- self.generate_template(
+ self._generate_template(
os.path.join(self.dir, "script.py.mako"),
path,
up_revision=str(revid),
@@ -208,18 +299,24 @@ class ScriptDirectory(object):
**kw
)
if refresh:
- script = Script.from_path(path)
+ script = Script._from_path(path)
self._revision_map[script.revision] = script
if script.down_revision:
self._revision_map[script.down_revision].\
add_nextrev(script.revision)
return script
else:
- return revid
+ return None
class Script(object):
- """Represent a single revision file in a ``versions/`` directory."""
+ """Represent a single revision file in a ``versions/`` directory.
+
+ The :class:`.Script` instance is returned by methods
+ such as :meth:`.ScriptDirectory.iterate_revisions`.
+
+ """
+
nextrev = frozenset()
def __init__(self, module, rev_id, path):
@@ -228,8 +325,21 @@ class Script(object):
self.path = path
self.down_revision = getattr(module, 'down_revision', None)
+ revision = None
+ """The string revision number for this :class:`.Script` instance."""
+
+ module = None
+ """The Python module representing the actual script itself."""
+
+ path = None
+ """Filesystem path of the script."""
+
+ down_revision = None
+ """The ``down_revision`` identifier within the migration script."""
+
@property
def doc(self):
+ """Return the docstring given in the script."""
return re.split(r"\n\n", self.module.__doc__)[0]
def add_nextrev(self, rev):
@@ -237,10 +347,25 @@ class Script(object):
@property
def is_head(self):
+ """Return True if this :class:`.Script` is a 'head' revision.
+
+ This is determined based on whether any other :class:`.Script`
+ within the :class:`.ScriptDirectory` refers to this
+ :class:`.Script`. Multiple heads can be present.
+
+ """
return not bool(self.nextrev)
@property
def is_branch_point(self):
+ """Return True if this :class:`.Script` is a branch point.
+
+ A branchpoint is defined as a :class:`.Script` which is referred
+ to by more than one succeeding :class:`.Script`, that is more
+ than one :class:`.Script` has a `down_revision` identifier pointing
+ here.
+
+ """
return len(self.nextrev) > 1
def __str__(self):
@@ -252,12 +377,12 @@ class Script(object):
self.doc)
@classmethod
- def from_path(cls, path):
+ def _from_path(cls, path):
dir_, filename = os.path.split(path)
- return cls.from_filename(dir_, filename)
+ return cls._from_filename(dir_, filename)
@classmethod
- def from_filename(cls, dir_, filename):
+ def _from_filename(cls, dir_, filename):
m = _rev_file.match(filename)
if not m:
return None
diff --git a/docs/build/api.rst b/docs/build/api.rst
index def71a3..de8f37e 100644
--- a/docs/build/api.rst
+++ b/docs/build/api.rst
@@ -1,3 +1,5 @@
+.. _api:
+
===========
API Details
===========
@@ -40,26 +42,36 @@ 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
out SQL to a file.
-env.py Directives
-=================
+The Environment Context
+=======================
-This section covers the objects that are generally used within an
-``env.py`` environmental configuration script. Alembic normally generates
-this script for you; it is however made available locally within the migrations
-environment so that it can be customized.
+The :class:`.EnvironmentContext` class provides most of the
+API used within an ``env.py`` script. Within ``env.py``,
+the instantated :class:`.EnvironmentContext` is made available
+via a special *proxy module* called ``alembic.context``. That is,
+you can import ``alembic.context`` like a regular Python module,
+and each name you call upon it is ultimately routed towards the
+current :class:`.EnvironmentContext` in use.
In particular, the key method used within ``env.py`` is :meth:`.EnvironmentContext.configure`,
which establishes all the details about how the database will be accessed.
-
-.. autofunction:: sqlalchemy.engine.engine_from_config
-
.. automodule:: alembic.environment
:members:
+The Migration Context
+=====================
+
.. automodule:: alembic.migration
:members:
+The Operations Object
+=====================
+
+Within migration scripts, actual database migration operations are handled
+via an instance of :class:`.Operations`. See :ref:`ops` for an overview
+of this object.
+
Commands
=========
@@ -75,31 +87,68 @@ object, as in::
alembic_cfg = Config("/path/to/yourapp/alembic.ini")
command.upgrade(alembic_cfg, "head")
+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`
+classes directly.
+
.. currentmodule:: alembic.command
.. automodule:: alembic.command
:members:
- :undoc-members:
Configuration
==============
+The :class:`.Config` object represents the configuration
+passed to the Alembic environment. From an API usage perspective,
+it is needed for the following use cases:
+
+* to create a :class:`.ScriptDirectory`, which allows you to work
+ with the actual script files in a migration environment
+* to create an :class:`.EnvironmentContext`, which allows you to
+ actually run the ``env.py`` module within the migration environment
+* to programatically run any of the commands in the :mod:`alembic.command`
+ module.
+
+The :class:`.Config` is *not* needed for these cases:
+
+* to instantiate a :class:`.MigrationContext` directly - this object
+ only needs a SQLAlchemy connection or dialect name.
+* to instantiate a :class:`.Operations` object - this object only
+ needs a :class:`.MigrationContext`.
+
.. currentmodule:: alembic.config
.. automodule:: alembic.config
:members:
- :undoc-members:
+Script Directory
+================
-Internals
-=========
+The :class:`.ScriptDirectory` object provides programmatic access
+to the Alembic version files present in the filesystem.
.. automodule:: alembic.script
:members:
- :undoc-members:
+
+Autogeneration
+==============
+
+Alembic 0.3 introduces a small portion of the autogeneration system
+as a public API.
+
+.. autofunction:: alembic.autogenerate.compare_metadata
DDL Internals
--------------
+=============
+
+These are some of the constructs used to generate migration
+instructions. The APIs here build off of the :class:`sqlalchemy.schema.DDLElement`
+and :mod:`sqlalchemy.ext.compiler` systems.
+
+For programmatic usage of Alembic's migration directives, the easiest
+route is to use the higher level functions given by :mod:`alembic.operations`.
.. automodule:: alembic.ddl
:members:
@@ -114,7 +163,7 @@ DDL Internals
:undoc-members:
MySQL
-^^^^^
+-----
.. automodule:: alembic.ddl.mysql
:members:
@@ -122,7 +171,7 @@ MySQL
:show-inheritance:
MS-SQL
-^^^^^^
+------
.. automodule:: alembic.ddl.mssql
:members:
@@ -130,7 +179,7 @@ MS-SQL
:show-inheritance:
Postgresql
-^^^^^^^^^^
+----------
.. automodule:: alembic.ddl.postgresql
:members:
@@ -138,7 +187,7 @@ Postgresql
:show-inheritance:
SQLite
-^^^^^^
+------
.. automodule:: alembic.ddl.sqlite
:members:
diff --git a/docs/build/conf.py b/docs/build/conf.py
index 44dde4b..2bb0961 100644
--- a/docs/build/conf.py
+++ b/docs/build/conf.py
@@ -206,6 +206,8 @@ latex_documents = [
#{'python': ('http://docs.python.org/3.2', None)}
+autoclass_content = "both"
+
intersphinx_mapping = {
'sqla':('http://www.sqlalchemy.org/docs/', None),
}
diff --git a/docs/build/front.rst b/docs/build/front.rst
index bbc6437..cee535c 100644
--- a/docs/build/front.rst
+++ b/docs/build/front.rst
@@ -50,6 +50,36 @@ is installed, in addition to other dependencies. Alembic will work with
SQLAlchemy as of version **0.6**, though with a limited featureset.
The latest version of SQLAlchemy within the **0.7** series is strongly recommended.
+Upgrading from Alembic 0.2 to 0.3
+=================================
+
+Alembic 0.3 is mostly identical to version 0.2 except for some API
+changes, allowing better programmatic access and less ambiguity
+between public and private methods. In particular:
+
+* :class:`.ScriptDirectory` now features these methods - the old
+ versions have been removed unless noted:
+
+ * :meth:`.ScriptDirectory.iterate_revisions()`
+ * :meth:`.ScriptDirectory.get_current_head()` (old name ``_current_head`` is available)
+ * :meth:`.ScriptDirectory.get_heads()`
+ * :meth:`.ScriptDirectory.get_base()`
+ * :meth:`.ScriptDirectory.generate_revision()`
+ * :meth:`.ScriptDirectory.get_revision()` (old name ``_get_rev`` is available)
+ * :meth:`.ScriptDirectory.as_revision_number()` (old name ``_as_rev_number`` is available)
+
+* :meth:`.MigrationContext.get_current_revision()` (old name ``_current_rev`` remains available)
+
+* Methods which have been made private include ``ScriptDirectory._copy_file()``,
+ ``ScriptDirectory._generate_template()``, ``ScriptDirectory._upgrade_revs()``,
+ ``ScriptDirectory._downgrade_revs()``. ``autogenerate._produce_migration_diffs``.
+ It's pretty unlikely that end-user applications
+ were using these directly.
+
+See the newly cleaned up :ref:`api` documentation for what are hopefully clearly
+laid out use cases for API usage, particularly being able to get at the revision
+information in a database as well as a script directory.
+
Upgrading from Alembic 0.1 to 0.2
=================================
diff --git a/tests/__init__.py b/tests/__init__.py
index e8baba8..d3193ae 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -121,6 +121,10 @@ def ne_(a, b, msg=None):
"""Assert a != b, with repr messaging on failure."""
assert a != b, msg or "%r == %r" % (a, b)
+def is_(a, b, msg=None):
+ """Assert a is b, with repr messaging on failure."""
+ assert a is b, msg or "%r is not %r" % (a, b)
+
def assert_raises_message(except_cls, msg, callable_, *args, **kwargs):
try:
callable_(*args, **kwargs)
@@ -294,7 +298,7 @@ def write_script(scriptdir, rev_id, content):
pyc_path = util.pyc_file_from_path(path)
if os.access(pyc_path, os.F_OK):
os.unlink(pyc_path)
- script = Script.from_path(path)
+ script = Script._from_path(path)
old = scriptdir._revision_map[script.revision]
if old.down_revision != script.down_revision:
raise Exception("Can't change down_revision "
@@ -309,7 +313,7 @@ def three_rev_fixture(cfg):
c = util.rev_id()
script = ScriptDirectory.from_config(cfg)
- script.generate_rev(a, "revision a", refresh=True)
+ script.generate_revision(a, "revision a", refresh=True)
write_script(script, a, """
revision = '%s'
down_revision = None
@@ -324,7 +328,7 @@ def downgrade():
""" % a)
- script.generate_rev(b, "revision b", refresh=True)
+ script.generate_revision(b, "revision b", refresh=True)
write_script(script, b, """
revision = '%s'
down_revision = '%s'
@@ -339,7 +343,7 @@ def downgrade():
""" % (b, a))
- script.generate_rev(c, "revision c", refresh=True)
+ script.generate_revision(c, "revision c", refresh=True)
write_script(script, c, """
revision = '%s'
down_revision = '%s'
diff --git a/tests/test_autogenerate.py b/tests/test_autogenerate.py
index e03ceea..9a1f6e7 100644
--- a/tests/test_autogenerate.py
+++ b/tests/test_autogenerate.py
@@ -129,7 +129,7 @@ class AutogenerateDiffTest(TestCase):
diffs = []
autogenerate._produce_net_changes(connection, metadata, diffs,
self.autogen_context)
-
+
eq_(
diffs[0],
('add_table', metadata.tables['item'])
@@ -177,7 +177,7 @@ class AutogenerateDiffTest(TestCase):
}
)
template_args = {}
- autogenerate.produce_migration_diffs(context, template_args, set())
+ autogenerate._produce_migration_diffs(context, template_args, set())
eq_(re.sub(r"u'", "'", template_args['upgrades']),
"""### commands auto generated by Alembic - please adjust! ###
pass
@@ -192,7 +192,7 @@ class AutogenerateDiffTest(TestCase):
metadata = self.m2
template_args = {}
- autogenerate.produce_migration_diffs(self.context, template_args, set())
+ autogenerate._produce_migration_diffs(self.context, template_args, set())
eq_(re.sub(r"u'", "'", template_args['upgrades']),
"""### commands auto generated by Alembic - please adjust! ###
op.create_table('item',
@@ -320,7 +320,7 @@ class AutogenerateDiffOrderTest(TestCase):
'sqlalchemy_module_prefix':'sa.'
}
)
-
+
connection = empty_context.bind
cls.autogen_empty_context = {
'imports':set(),
@@ -332,7 +332,7 @@ class AutogenerateDiffOrderTest(TestCase):
@classmethod
def teardown_class(cls):
clear_staging_env()
-
+
def test_diffs_order(self):
"""
Added in order to test that child tables(tables with FKs) are generated
@@ -342,10 +342,10 @@ class AutogenerateDiffOrderTest(TestCase):
metadata = self.m4
connection = self.empty_context.bind
diffs = []
-
+
autogenerate._produce_net_changes(connection, metadata, diffs,
self.autogen_empty_context)
-
+
eq_(diffs[0][0], 'add_table')
eq_(diffs[0][1].name, "parent")
eq_(diffs[1][0], 'add_table')
diff --git a/tests/test_postgresql.py b/tests/test_postgresql.py
index 3eea778..38560ef 100644
--- a/tests/test_postgresql.py
+++ b/tests/test_postgresql.py
@@ -18,7 +18,7 @@ class PGOfflineEnumTest(TestCase):
self.rid = rid = util.rev_id()
self.script = script = ScriptDirectory.from_config(cfg)
- script.generate_rev(rid, None, refresh=True)
+ script.generate_revision(rid, None, refresh=True)
def tearDown(self):
clear_staging_env()
diff --git a/tests/test_revision_create.py b/tests/test_revision_create.py
index 2bdca93..eaf1859 100644
--- a/tests/test_revision_create.py
+++ b/tests/test_revision_create.py
@@ -1,4 +1,4 @@
-from tests import clear_staging_env, staging_env, eq_, ne_
+from tests import clear_staging_env, staging_env, eq_, ne_, is_
from alembic import util
import os
@@ -16,20 +16,20 @@ def test_002_rev_ids():
ne_(abc, def_)
def test_003_heads():
- eq_(env._get_heads(), [])
+ eq_(env.get_heads(), [])
def test_004_rev():
- script = env.generate_rev(abc, "this is a message", refresh=True)
+ script = env.generate_revision(abc, "this is a message", refresh=True)
eq_(script.doc, "this is a message")
eq_(script.revision, abc)
eq_(script.down_revision, None)
assert os.access(
os.path.join(env.dir, 'versions', '%s_this_is_a_message.py' % abc), os.F_OK)
assert callable(script.module.upgrade)
- eq_(env._get_heads(), [abc])
+ eq_(env.get_heads(), [abc])
def test_005_nextrev():
- script = env.generate_rev(def_, "this is the next rev", refresh=True)
+ script = env.generate_revision(def_, "this is the next rev", refresh=True)
assert os.access(
os.path.join(env.dir, 'versions', '%s_this_is_the_next_rev.py' % def_), os.F_OK)
eq_(script.revision, def_)
@@ -38,7 +38,7 @@ def test_005_nextrev():
assert script.module.down_revision == abc
assert callable(script.module.upgrade)
assert callable(script.module.downgrade)
- eq_(env._get_heads(), [def_])
+ eq_(env.get_heads(), [def_])
def test_006_from_clean_env():
# test the environment so far with a
@@ -50,17 +50,18 @@ def test_006_from_clean_env():
eq_(abc_rev.nextrev, set([def_]))
eq_(abc_rev.revision, abc)
eq_(def_rev.down_revision, abc)
- eq_(env._get_heads(), [def_])
+ eq_(env.get_heads(), [def_])
def test_007_no_refresh():
- script = env.generate_rev(util.rev_id(), "dont' refresh")
- ne_(script, env._as_rev_number("head"))
+ rid = util.rev_id()
+ script = env.generate_revision(rid, "dont' refresh")
+ is_(script, None)
env2 = staging_env(create=False)
- eq_(script, env2._as_rev_number("head"))
+ eq_(env2._as_rev_number("head"), rid)
def test_008_long_name():
rid = util.rev_id()
- script = env.generate_rev(rid,
+ script = env.generate_revision(rid,
"this is a really long name with "
"lots of characters and also "
"I'd like it to\nhave\nnewlines")
diff --git a/tests/test_revision_paths.py b/tests/test_revision_paths.py
index fd09a85..127fda8 100644
--- a/tests/test_revision_paths.py
+++ b/tests/test_revision_paths.py
@@ -6,11 +6,11 @@ def setup():
global env
env = staging_env()
global a, b, c, d, e
- a = env.generate_rev(util.rev_id(), None, refresh=True)
- b = env.generate_rev(util.rev_id(), None, refresh=True)
- c = env.generate_rev(util.rev_id(), None, refresh=True)
- d = env.generate_rev(util.rev_id(), None, refresh=True)
- e = env.generate_rev(util.rev_id(), None, refresh=True)
+ a = env.generate_revision(util.rev_id(), None, refresh=True)
+ b = env.generate_revision(util.rev_id(), None, refresh=True)
+ c = env.generate_revision(util.rev_id(), None, refresh=True)
+ d = env.generate_revision(util.rev_id(), None, refresh=True)
+ e = env.generate_revision(util.rev_id(), None, refresh=True)
def teardown():
clear_staging_env()
@@ -19,7 +19,7 @@ def teardown():
def test_upgrade_path():
eq_(
- env.upgrade_from(e.revision, c.revision, None),
+ env._upgrade_revs(e.revision, c.revision),
[
(d.module.upgrade, c.revision, d.revision),
(e.module.upgrade, d.revision, e.revision),
@@ -27,7 +27,7 @@ def test_upgrade_path():
)
eq_(
- env.upgrade_from(c.revision, None, None),
+ env._upgrade_revs(c.revision, None),
[
(a.module.upgrade, None, a.revision),
(b.module.upgrade, a.revision, b.revision),
@@ -38,7 +38,7 @@ def test_upgrade_path():
def test_downgrade_path():
eq_(
- env.downgrade_to(c.revision, e.revision, None),
+ env._downgrade_revs(c.revision, e.revision),
[
(e.module.downgrade, e.revision, e.down_revision),
(d.module.downgrade, d.revision, d.down_revision),
@@ -46,7 +46,7 @@ def test_downgrade_path():
)
eq_(
- env.downgrade_to(None, c.revision, None),
+ env._downgrade_revs(None, c.revision),
[
(c.module.downgrade, c.revision, c.down_revision),
(b.module.downgrade, b.revision, b.down_revision),
diff --git a/tests/test_versioning.py b/tests/test_versioning.py
index 2f19157..989d612 100644
--- a/tests/test_versioning.py
+++ b/tests/test_versioning.py
@@ -16,7 +16,7 @@ class VersioningTest(unittest.TestCase):
c = util.rev_id()
script = ScriptDirectory.from_config(self.cfg)
- script.generate_rev(a, None, refresh=True)
+ script.generate_revision(a, None, refresh=True)
write_script(script, a, """
revision = '%s'
down_revision = None
@@ -31,7 +31,7 @@ class VersioningTest(unittest.TestCase):
""" % a)
- script.generate_rev(b, None, refresh=True)
+ script.generate_revision(b, None, refresh=True)
write_script(script, b, """
revision = '%s'
down_revision = '%s'
@@ -46,7 +46,7 @@ class VersioningTest(unittest.TestCase):
""" % (b, a))
- script.generate_rev(c, None, refresh=True)
+ script.generate_revision(c, None, refresh=True)
write_script(script, c, """
revision = '%s'
down_revision = '%s'
@@ -117,7 +117,7 @@ class VersionNameTemplateTest(unittest.TestCase):
self.cfg.set_main_option("file_template", "myfile_%%(slug)s")
script = ScriptDirectory.from_config(self.cfg)
a = util.rev_id()
- script.generate_rev(a, "some message", refresh=True)
+ script.generate_revision(a, "some message", refresh=True)
write_script(script, a, """
revision = '%s'
down_revision = None
@@ -141,7 +141,7 @@ class VersionNameTemplateTest(unittest.TestCase):
self.cfg.set_main_option("file_template", "%%(rev)s")
script = ScriptDirectory.from_config(self.cfg)
a = util.rev_id()
- script.generate_rev(a, None, refresh=True)
+ script.generate_revision(a, None, refresh=True)
write_script(script, a, """
down_revision = None
@@ -164,7 +164,7 @@ class VersionNameTemplateTest(unittest.TestCase):
self.cfg.set_main_option("file_template", "%%(slug)s_%%(rev)s")
script = ScriptDirectory.from_config(self.cfg)
a = util.rev_id()
- script.generate_rev(a, "foobar", refresh=True)
+ script.generate_revision(a, "foobar", refresh=True)
assert_raises_message(
util.CommandError,
"Could not determine revision id from filename foobar_%s.py. "