From: Mike Bayer Date: Sat, 2 Jun 2012 18:42:09 +0000 (-0400) Subject: - [feature] New config argument X-Git-Tag: rel_0_3_3 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d4094ff7a76cd4438b15242c7c883333235fc0c7;p=thirdparty%2Fsqlalchemy%2Falembic.git - [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". --- diff --git a/CHANGES b/CHANGES index e075dcf1..8e79cb5d 100644 --- a/CHANGES +++ b/CHANGES @@ -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. diff --git a/alembic/command.py b/alembic/command.py index 98a054b5..8bb82e95 100644 --- a/alembic/command.py +++ b/alembic/command.py @@ -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) diff --git a/alembic/environment.py b/alembic/environment.py index 4aeb8116..9db519f7 100644 --- a/alembic/environment.py +++ b/alembic/environment.py @@ -206,6 +206,7 @@ class EnvironmentContext(object): output_buffer=None, starting_rev=None, tag=None, + template_args=None, target_metadata=None, compare_type=False, compare_server_default=False, @@ -273,6 +274,12 @@ class EnvironmentContext(object): 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'``. @@ -398,6 +405,8 @@ class EnvironmentContext(object): 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 diff --git a/alembic/templates/generic/alembic.ini.mako b/alembic/templates/generic/alembic.ini.mako index 199c8a4e..386e0467 100644 --- a/alembic/templates/generic/alembic.ini.mako +++ b/alembic/templates/generic/alembic.ini.mako @@ -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 diff --git a/alembic/templates/multidb/alembic.ini.mako b/alembic/templates/multidb/alembic.ini.mako index 7d33ba22..ab84b8ab 100644 --- a/alembic/templates/multidb/alembic.ini.mako +++ b/alembic/templates/multidb/alembic.ini.mako @@ -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] diff --git a/alembic/templates/pylons/alembic.ini.mako b/alembic/templates/pylons/alembic.ini.mako index 18041dce..2fa0e68b 100644 --- a/alembic/templates/pylons/alembic.ini.mako +++ b/alembic/templates/pylons/alembic.ini.mako @@ -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 ! \ No newline at end of file diff --git a/alembic/util.py b/alembic/util.py index fb0c51a5..bbfe9279 100644 --- a/alembic/util.py +++ b/alembic/util.py @@ -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) diff --git a/docs/build/tutorial.rst b/docs/build/tutorial.rst index af3380ce..a1627334 100644 --- a/docs/build/tutorial.rst +++ b/docs/build/tutorial.rst @@ -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 @@ -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 `_. diff --git a/tests/__init__.py b/tests/__init__.py index 7f833000..3d9455b4 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -181,6 +181,12 @@ def op_fixture(dialect='default', as_sql=False): 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 = """ @@ -230,13 +236,14 @@ datefmt = %%H:%%M:%%S """ % (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 @@ -262,7 +269,7 @@ keys = generic 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() diff --git a/tests/test_revision_create.py b/tests/test_revision_create.py index eaf1859d..03159032 100644 --- a/tests/test_revision_create.py +++ b/tests/test_revision_create.py @@ -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() \ No newline at end of file