Skip to content

Commit

Permalink
- [feature] New config argument
Browse files Browse the repository at this point in the history
  "revision_environment=true", causes env.py to
  be run unconditionally when the "revision" command
  is run, to support script.py.mako templates with
  dependencies on custom "template_args".

- [feature] Added "template_args" option to configure()
  so that an env.py can add additional arguments
  to the template context when running the
  "revision" command.  This requires either --autogenerate
  or the configuration directive "revision_environment=true".
  • Loading branch information
zzzeek committed Jun 2, 2012
1 parent bb160db commit d4094ff
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 77 deletions.
12 changes: 12 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
0.3.3
=====
- [feature] New config argument
"revision_environment=true", causes env.py to
be run unconditionally when the "revision" command
is run, to support script.py.mako templates with
dependencies on custom "template_args".

- [feature] Added "template_args" option to configure()
so that an env.py can add additional arguments
to the template context when running the
"revision" command. This requires either --autogenerate
or the configuration directive "revision_environment=true".

- [bug] Added "type" argument to op.drop_constraint(),
and implemented full constraint drop support for
MySQL. CHECK and undefined raise an error.
Expand Down
14 changes: 12 additions & 2 deletions alembic/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,24 +60,34 @@ def init(config, directory, template='generic'):
util.msg("Please edit configuration/connection/logging "\
"settings in %r before proceeding." % config_file)

def revision(config, message=None, autogenerate=False):
def revision(config, message=None, autogenerate=False, environment=False):
"""Create a new revision file."""

script = ScriptDirectory.from_config(config)
template_args = {}
imports = set()

if util.asbool(config.get_main_option("revision_environment")):
environment = True

if autogenerate:
environment = True
util.requires_07("autogenerate")
def retrieve_migrations(rev, context):
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)
return []
elif environment:
def retrieve_migrations(rev, context):
pass

if environment:
with EnvironmentContext(
config,
script,
fn = retrieve_migrations
fn = retrieve_migrations,
template_args = template_args,
):
script.run_env()
script.generate_revision(util.rev_id(), message, **template_args)
Expand Down
9 changes: 9 additions & 0 deletions alembic/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ def configure(self,
output_buffer=None,
starting_rev=None,
tag=None,
template_args=None,
target_metadata=None,
compare_type=False,
compare_server_default=False,
Expand Down Expand Up @@ -273,6 +274,12 @@ def configure(self,
when using ``--sql`` mode.
:param tag: a string tag for usage by custom ``env.py`` scripts.
Set via the ``--tag`` option, can be overridden here.
:param template_args: dictionary of template arguments which
will be added to the template argument environment when
running the "revision" command. Note that the script environment
is only run within the "revision" command if the --autogenerate
option is used, or if the option "revision_environment=true"
is present in the alembic.ini file. New in 0.3.3.
:param version_table: The name of the Alembic version table.
The default is ``'alembic_version'``.
Expand Down Expand Up @@ -398,6 +405,8 @@ def my_compare_server_default(context, inspected_column,
opts['starting_rev'] = starting_rev
if tag:
opts['tag'] = tag
if template_args and 'template_args' in opts:
opts['template_args'].update(template_args)
opts['target_metadata'] = target_metadata
opts['upgrade_token'] = upgrade_token
opts['downgrade_token'] = downgrade_token
Expand Down
4 changes: 4 additions & 0 deletions alembic/templates/generic/alembic.ini.mako
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ script_location = ${script_location}
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false

sqlalchemy.url = driver://user:pass@localhost/dbname


Expand Down
4 changes: 4 additions & 0 deletions alembic/templates/multidb/alembic.ini.mako
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ script_location = ${script_location}
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false

databases = engine1, engine2

[engine1]
Expand Down
4 changes: 4 additions & 0 deletions alembic/templates/pylons/alembic.ini.mako
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ script_location = ${script_location}
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false

pylons_config_file = ./development.ini

# that's it !
4 changes: 4 additions & 0 deletions alembic/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ def obfuscate_url_pw(u):
u.password = 'XXXXX'
return str(u)

def asbool(value):
return value is not None and \
value.lower() == 'true'

def warn(msg):
warnings.warn(msg)

Expand Down
7 changes: 7 additions & 0 deletions docs/build/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ The file generated with the "generic" configuration looks like::
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false

sqlalchemy.url = driver://user:pass@localhost/dbname

# Logging configuration
Expand Down Expand Up @@ -190,6 +194,9 @@ This file contains the following features:
a file that can be customized by the developer. A multiple
database configuration may respond to multiple keys here, or may reference other sections
of the file.
* ``revision_environment`` - this is a flag which when set to the value 'true', will indicate
that the migration environment script ``env.py`` should be run unconditionally when
generating new revision files (new in 0.3.3).
* ``[loggers]``, ``[handlers]``, ``[formatters]``, ``[logger_*]``, ``[handler_*]``,
``[formatter_*]`` - these sections are all part of Python's standard logging configuration,
the mechanics of which are documented at `Configuration File Format <http://docs.python.org/library/logging.config.html#configuration-file-format>`_.
Expand Down
11 changes: 9 additions & 2 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,12 @@ def assert_contains(self, sql):
alembic.op._proxy = Operations(context)
return context

def script_file_fixture(txt):
dir_ = os.path.join(staging_directory, 'scripts')
path = os.path.join(dir_, "script.py.mako")
with open(path, 'w') as f:
f.write(txt)

def env_file_fixture(txt):
dir_ = os.path.join(staging_directory, 'scripts')
txt = """
Expand Down Expand Up @@ -230,13 +236,14 @@ class = StreamHandler
""" % (dir_, dir_))


def _no_sql_testing_config(dialect="postgresql"):
def _no_sql_testing_config(dialect="postgresql", directives=""):
"""use a postgresql url with no host so that connections guaranteed to fail"""
dir_ = os.path.join(staging_directory, 'scripts')
return _write_config_file("""
[alembic]
script_location = %s
sqlalchemy.url = %s://
%s
[loggers]
keys = root
Expand All @@ -262,7 +269,7 @@ class = StreamHandler
format = %%(levelname)-5.5s [%%(name)s] %%(message)s
datefmt = %%H:%%M:%%S
""" % (dir_, dialect))
""" % (dir_, dialect, directives))

def _write_config_file(text):
cfg = _testing_config()
Expand Down
195 changes: 122 additions & 73 deletions tests/test_revision_create.py
Original file line number Diff line number Diff line change
@@ -1,77 +1,126 @@
from tests import clear_staging_env, staging_env, eq_, ne_, is_
from tests import _no_sql_testing_config, env_file_fixture, script_file_fixture
from alembic import command
from alembic.script import ScriptDirectory
from alembic.environment import EnvironmentContext
from alembic import util
import os
import unittest

class GeneralOrderedTests(unittest.TestCase):
def test_001_environment(self):
assert_set = set(['env.py', 'script.py.mako', 'README'])
eq_(
assert_set.intersection(os.listdir(env.dir)),
assert_set
)

def test_002_rev_ids(self):
global abc, def_
abc = util.rev_id()
def_ = util.rev_id()
ne_(abc, def_)

def test_003_heads(self):
eq_(env.get_heads(), [])

def test_004_rev(self):
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])

def test_005_nextrev(self):
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_)
eq_(script.down_revision, abc)
eq_(env._revision_map[abc].nextrev, set([def_]))
assert script.module.down_revision == abc
assert callable(script.module.upgrade)
assert callable(script.module.downgrade)
eq_(env.get_heads(), [def_])

def test_006_from_clean_env(self):
# test the environment so far with a
# new ScriptDirectory instance.

env = staging_env(create=False)
abc_rev = env._revision_map[abc]
def_rev = env._revision_map[def_]
eq_(abc_rev.nextrev, set([def_]))
eq_(abc_rev.revision, abc)
eq_(def_rev.down_revision, abc)
eq_(env.get_heads(), [def_])

def test_007_no_refresh(self):
rid = util.rev_id()
script = env.generate_revision(rid, "dont' refresh")
is_(script, None)
env2 = staging_env(create=False)
eq_(env2._as_rev_number("head"), rid)

def test_008_long_name(self):
rid = util.rev_id()
script = env.generate_revision(rid,
"this is a really long name with "
"lots of characters and also "
"I'd like it to\nhave\nnewlines")
assert os.access(
os.path.join(env.dir, 'versions', '%s_this_is_a_really_lon.py' % rid), os.F_OK)

@classmethod
def setup_class(cls):
global env
env = staging_env()

@classmethod
def teardown_class(cls):
clear_staging_env()

class TemplateArgsTest(unittest.TestCase):
def setUp(self):
env = staging_env()
self.cfg = _no_sql_testing_config(
directives="\nrevision_environment=true\n"
)

def tearDown(self):
clear_staging_env()

def test_args_propagate(self):
config = _no_sql_testing_config()
script = ScriptDirectory.from_config(config)
template_args = {"x":"x1", "y":"y1", "z":"z1"}
env = EnvironmentContext(
config,
script,
template_args = template_args
)
mig_env = env.configure(dialect_name="sqlite",
template_args={"y":"y2", "q":"q1"})
eq_(
template_args,
{"x":"x1", "y":"y2", "z":"z1", "q":"q1"}
)

def test_tmpl_args_revision(self):
env_file_fixture("""
context.configure(dialect_name='sqlite', template_args={"somearg":"somevalue"})
""")
script_file_fixture("""
# somearg: ${somearg}
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
""")
command.revision(self.cfg, message="some rev")
script = ScriptDirectory.from_config(self.cfg)
rev = script.get_revision('head')
text = open(rev.path).read()
assert "somearg: somevalue" in text

def test_001_environment():
assert_set = set(['env.py', 'script.py.mako', 'README'])
eq_(
assert_set.intersection(os.listdir(env.dir)),
assert_set
)

def test_002_rev_ids():
global abc, def_
abc = util.rev_id()
def_ = util.rev_id()
ne_(abc, def_)

def test_003_heads():
eq_(env.get_heads(), [])

def test_004_rev():
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])

def test_005_nextrev():
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_)
eq_(script.down_revision, abc)
eq_(env._revision_map[abc].nextrev, set([def_]))
assert script.module.down_revision == abc
assert callable(script.module.upgrade)
assert callable(script.module.downgrade)
eq_(env.get_heads(), [def_])

def test_006_from_clean_env():
# test the environment so far with a
# new ScriptDirectory instance.

env = staging_env(create=False)
abc_rev = env._revision_map[abc]
def_rev = env._revision_map[def_]
eq_(abc_rev.nextrev, set([def_]))
eq_(abc_rev.revision, abc)
eq_(def_rev.down_revision, abc)
eq_(env.get_heads(), [def_])

def test_007_no_refresh():
rid = util.rev_id()
script = env.generate_revision(rid, "dont' refresh")
is_(script, None)
env2 = staging_env(create=False)
eq_(env2._as_rev_number("head"), rid)

def test_008_long_name():
rid = util.rev_id()
script = env.generate_revision(rid,
"this is a really long name with "
"lots of characters and also "
"I'd like it to\nhave\nnewlines")
assert os.access(
os.path.join(env.dir, 'versions', '%s_this_is_a_really_lon.py' % rid), os.F_OK)


def setup():
global env
env = staging_env()

def teardown():
clear_staging_env()

0 comments on commit d4094ff

Please sign in to comment.