summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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. "