Commits

Mike Bayer  committed 8332e56

- move to 0.3 as we are changing API
- [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.

  • Participants
  • Parent commits 67fda40
  • Tags rel_0_3_0

Comments (0)

Files changed (17)

+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
   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,
   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

File alembic/__init__.py

 from os import path
 
-__version__ = '0.2.2'
+__version__ = '0.3.0'
 
 package_dir = path.abspath(path.dirname(__file__))
 

File alembic/autogenerate.py

 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:
                 "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))
             _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
         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:
 
     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
                     )

File alembic/command.py

 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"""
             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
             )
     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):
         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,
             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,
             for rev in sc.nextrev:
                 print "%s -> %s" % (
                     " " * len(str(sc.down_revision)),
-                    script._get_rev(rev)
+                    script.get_revision(rev)
                 )
 
 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
             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,

File alembic/config.py

     
         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")
 

File alembic/environment.py

     :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
     """
 
     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
     def get_impl(self):
         return self.get_context().impl
 
-configure = EnvironmentContext

File alembic/migration.py

             )
 
 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):
         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:
             _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
                     )
 
     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
                 _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):
         """
         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
 
                                     inspector_column, 
                                     metadata_column)
 
-    def compare_server_default(self, inspector_column, 
+    def _compare_server_default(self, inspector_column, 
                             metadata_column, 
                             rendered_metadata_default):
 

File alembic/script.py

 
 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
 
     @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')
         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:
             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
                 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
 
     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')
 
     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_:
         )
         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:
         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, 
             **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),
             **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):
         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):
 
     @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):
                         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

File docs/build/api.rst

+.. _api:
+
 ===========
 API Details
 ===========
 :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
 =========
 
     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:
     :undoc-members:
 
 MySQL
-^^^^^
+-----
 
 .. automodule:: alembic.ddl.mysql
     :members:
     :show-inheritance:
 
 MS-SQL
-^^^^^^
+------
 
 .. automodule:: alembic.ddl.mssql
     :members:
     :show-inheritance:
 
 Postgresql
-^^^^^^^^^^
+----------
 
 .. automodule:: alembic.ddl.postgresql
     :members:
     :show-inheritance:
 
 SQLite
-^^^^^^
+------
 
 .. automodule:: alembic.ddl.sqlite
     :members:

File docs/build/conf.py

 
 #{'python': ('http://docs.python.org/3.2', None)}
 
+autoclass_content = "both"
+
 intersphinx_mapping = {
     'sqla':('http://www.sqlalchemy.org/docs/', None), 
 }

File docs/build/front.rst

 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
 =================================
 

File tests/__init__.py

     """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)
     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 "
     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
 
 """ % 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'
 
 """ % (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'

File tests/test_autogenerate.py

         diffs = []
         autogenerate._produce_net_changes(connection, metadata, diffs, 
                                           self.autogen_context)
-        
+
         eq_(
             diffs[0],
             ('add_table', metadata.tables['item'])
             }
         )
         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
 
         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',
                 'sqlalchemy_module_prefix':'sa.'
             }
         )
-        
+
         connection = empty_context.bind
         cls.autogen_empty_context = {
             'imports':set(),
     @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
         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')

File tests/test_postgresql.py

         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()

File tests/test_revision_create.py

-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
 
     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_)
     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 
     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")

File tests/test_revision_paths.py

     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()
 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),
     )
 
     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),
 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),
     )
 
     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),

File tests/test_versioning.py

         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
 
     """ % a)
 
-        script.generate_rev(b, None, refresh=True)
+        script.generate_revision(b, None, refresh=True)
         write_script(script, b, """
     revision = '%s'
     down_revision = '%s'
 
     """ % (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'
         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
         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
 
         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. "