summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorolly <olly@ollycope.com>2014-02-02 17:40:54 +0000
committerolly <olly@ollycope.com>2014-02-02 17:40:54 +0000
commita85e367defdc9000b2dbd0b02d4b57376c95c269 (patch)
tree2a3bf77fa96a77b4e8962895f69a4b2e3b693b64
parent50ab10d45e17eb2372d5200c447299a3802a284b (diff)
parent0c7ed60c89fd7a570395ff3577d860ab96c5603c (diff)
downloadyoyo-a85e367defdc9000b2dbd0b02d4b57376c95c269.tar.gz
merged utc fix
-rw-r--r--CHANGELOG.rst7
-rw-r--r--fabfile.py2
-rw-r--r--setup.py13
-rw-r--r--tox.ini3
-rw-r--r--yoyo/__init__.py2
-rw-r--r--yoyo/scripts/migrate.py156
-rw-r--r--yoyo/tests/__init__.py38
-rw-r--r--yoyo/tests/test_cli_script.py71
-rw-r--r--yoyo/tests/test_migrations.py (renamed from yoyo/tests.py)40
9 files changed, 202 insertions, 130 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index f35e255..ae386c4 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,5 +1,12 @@
CHANGELOG
---------
+
+Version 4.2.3
+
+* Migrations are now datestamped with a UTC date
+
+* Fixes for installation and use under python 3
+
Version 4.2.2
* Migration scripts can start with ``from yoyo import step, transaction``.
diff --git a/fabfile.py b/fabfile.py
index ca56943..79595ac 100644
--- a/fabfile.py
+++ b/fabfile.py
@@ -150,7 +150,7 @@ def _check_release():
in a virtualenv
"""
with lcd(env.build_path):
- local("./bin/nosetests")
+ local("tox")
try:
local("virtualenv test_virtualenv")
local("./test_virtualenv/bin/pip install ./dist/*.tar.gz" % env)
diff --git a/setup.py b/setup.py
index 84938ff..ae5de43 100644
--- a/setup.py
+++ b/setup.py
@@ -5,6 +5,7 @@ import re
from setuptools import setup, find_packages
VERSIONFILE = "yoyo/__init__.py"
+install_requires = []
def get_version():
@@ -12,6 +13,11 @@ def get_version():
return re.search("^__version__\s*=\s*['\"]([^'\"]*)['\"]",
f.read().decode('UTF-8'), re.M).group(1)
+try:
+ import argparse # NOQA
+except ImportError:
+ install_requires.append('argparse')
+
def read(*path):
"""
@@ -32,11 +38,12 @@ setup(
packages=find_packages(),
include_package_data=True,
zip_safe=False,
+ install_requires=install_requires,
extras_require={
- 'mysql': [u'mysql-python'],
- 'postgres': [u'psycopg2'],
+ 'mysql': ['mysql-python'],
+ 'postgres': ['psycopg2'],
},
- tests_require=['sqlite3'],
+ tests_require=['sqlite3', 'mock'],
entry_points={
'console_scripts': [
'yoyo-migrate=yoyo.scripts.migrate:main'
diff --git a/tox.ini b/tox.ini
index 07887fb..40b4ffd 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,7 +1,8 @@
[tox]
-envlist = py26,py27,py33
+envlist = py26,py27,py32,py33
[testenv]
deps=
nose
+ mock
commands=nosetests []
diff --git a/yoyo/__init__.py b/yoyo/__init__.py
index b3ae572..03ac4ca 100644
--- a/yoyo/__init__.py
+++ b/yoyo/__init__.py
@@ -3,4 +3,4 @@ from yoyo.migrations import (read_migrations, initialize_connection, # noqa
default_migration_table, logger,
step, transaction)
-__version__ = '4.2.3dev'
+__version__ = '4.2.4dev'
diff --git a/yoyo/scripts/migrate.py b/yoyo/scripts/migrate.py
index ae53a59..cf31ee7 100644
--- a/yoyo/scripts/migrate.py
+++ b/yoyo/scripts/migrate.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
from __future__ import print_function
import logging
-import optparse
+import argparse
import os
import re
import sys
@@ -129,54 +129,52 @@ def prompt_migrations(conn, paramstyle, migrations, direction):
if m.choice == 'y')
-def make_optparser():
-
- optparser = optparse.OptionParser(usage="%prog apply|rollback|reapply "
- "<migrations> <database>")
- optparser.add_option(
- "-m", "--match", dest="match",
- help="Select migrations matching PATTERN "
- "(perl-compatible regular expression)", metavar='PATTERN',
- )
- optparser.add_option(
- "-a", "--all", dest="all", action="store_true",
- help="Select all migrations, regardless of whether "
- "they have been previously applied"
- )
- optparser.add_option(
- "-b", "--batch", dest="batch", action="store_true",
- help="Run in batch mode "
- "(don't ask before applying/rolling back each migration)"
- )
- optparser.add_option(
- "-v", dest="verbose", action="count",
- help="Verbose output. "
- "Use multiple times to increase level of verbosity"
- )
- optparser.add_option(
- "--verbosity", dest="verbosity_level", action="store", type="int",
- help="Set verbosity level (%d-%d)" % (min(verbosity_levels),
- max(verbosity_levels)),
- )
- optparser.add_option(
- "", "--force", dest="force", action="store_true",
- help="Force apply/rollback of steps even if previous steps have failed"
- )
- optparser.add_option(
- "-p", "--prompt-password", dest="prompt_password", action="store_true",
- help="Prompt for the database password"
- )
- optparser.add_option(
- "", "--no-cache", dest="cache", action="store_false", default=True,
- help="Don't cache database login credentials"
- )
- optparser.add_option(
- "", "--migration-table", dest="migration_table",
- action="store", default=None,
- help="Name of table to use for storing migration metadata"
- )
-
- return optparser
+def make_argparser():
+
+ min_verbosity = min(verbosity_levels)
+ max_verbosity = max(verbosity_levels)
+
+ argparser = argparse.ArgumentParser()
+ argparser.add_argument("command", choices=['apply', 'rollback', 'reapply'])
+ argparser.add_argument("migrations_dir",
+ help="Directory containing migration scripts")
+ argparser.add_argument("database", nargs="?", default=None,
+ help="Database, eg 'sqlite:///path/to/sqlite.db' "
+ "or 'postgresql://user@host/db'")
+
+ argparser.add_argument("-m", "--match",
+ help="Select migrations matching PATTERN "
+ "(perl-compatible regular expression)",
+ metavar='PATTERN')
+ argparser.add_argument("-a", "--all", dest="all", action="store_true",
+ help="Select all migrations, regardless of whether "
+ "they have been previously applied")
+ argparser.add_argument("-b", "--batch", dest="batch", action="store_true",
+ help="Run in batch mode (don't ask before "
+ "applying/rolling back each migration)")
+ argparser.add_argument("-v", dest="verbose", action="count",
+ default=min_verbosity,
+ help="Verbose output. Use multiple times "
+ "to increase level of verbosity")
+ argparser.add_argument("--verbosity", dest="verbosity_level",
+ type=int, default=min_verbosity,
+ help="Set verbosity level (%d-%d)" %
+ (min_verbosity, max_verbosity))
+ argparser.add_argument("-f", "--force", dest="force", action="store_true",
+ help="Force apply/rollback of steps even if "
+ "previous steps have failed")
+ argparser.add_argument("-p", "--prompt-password", dest="prompt_password",
+ action="store_true",
+ help="Prompt for the database password")
+ argparser.add_argument("--no-cache", dest="cache", action="store_false",
+ default=True,
+ help="Don't cache database login credentials")
+ argparser.add_argument("--migration-table", dest="migration_table",
+ action="store", default=None,
+ help="Name of table to use for storing "
+ "migration metadata")
+
+ return argparser
def configure_logging(level):
@@ -188,44 +186,33 @@ def configure_logging(level):
def main(argv=None):
- if argv is None:
- argv = sys.argv[1:]
+ argparser = make_argparser()
+ args = argparser.parse_args(argv)
- optparser = make_optparser()
- opts, args = optparser.parse_args(argv)
-
- if opts.verbosity_level:
- verbosity_level = opts.verbosity_level
+ if args.verbosity_level:
+ verbosity_level = args.verbosity_level
else:
- verbosity_level = opts.verbose
+ verbosity_level = args.verbose
verbosity_level = min(verbosity_level, max(verbosity_levels))
verbosity_level = max(verbosity_level, min(verbosity_levels))
configure_logging(verbosity_level)
- command = dburi = migrations_dir = None
- try:
- command, migrations_dir, dburi = args
- migrations_dir = os.path.normpath(os.path.abspath(migrations_dir))
- except ValueError:
- try:
- command, migrations_dir = args
- except ValueError:
- optparser.print_help()
- return
- dburi = None
+ command = args.command
+ migrations_dir = os.path.normpath(os.path.abspath(args.migrations_dir))
+ dburi = args.database
config_path = os.path.join(migrations_dir, '.yoyo-migrate')
config = readconfig(config_path)
- if dburi is None and opts.cache:
+ if dburi is None and args.cache:
try:
logger.debug("Looking up connection string for %r", migrations_dir)
dburi = config.get('DEFAULT', 'dburi')
except (ValueError, NoSectionError, NoOptionError):
pass
- if opts.migration_table:
- migration_table = opts.migration_table
+ if args.migration_table:
+ migration_table = args.migration_table
else:
try:
migration_table = config.get('DEFAULT', 'migration_table')
@@ -240,15 +227,12 @@ def main(argv=None):
config.set('DEFAULT', 'migration_table', migration_table)
if dburi is None:
- optparser.error(
+ argparser.error(
"Please specify command, migrations directory and "
"database connection string arguments"
)
- if command not in ['apply', 'rollback', 'reapply']:
- optparser.error("Invalid command")
-
- if opts.prompt_password:
+ if args.prompt_password:
password = getpass('Password for %s: ' % dburi)
scheme, username, _, host, port, database = parse_uri(dburi)
dburi = unparse_uri((scheme, username, password, host, port, database))
@@ -256,7 +240,7 @@ def main(argv=None):
# Cache the database this migration set is applied to so that subsequent
# runs don't need the dburi argument. Don't cache anything in batch mode -
# we can't prompt to find the user's preference.
- if opts.cache and not opts.batch:
+ if args.cache and not args.batch:
if not config.has_option('DEFAULT', 'dburi'):
response = prompt(
"Save connection string to %s for future migrations?\n"
@@ -284,35 +268,35 @@ def main(argv=None):
migrations = read_migrations(conn, paramstyle, migrations_dir,
migration_table=migration_table)
- if opts.match:
+ if args.match:
migrations = migrations.filter(
- lambda m: re.search(opts.match, m.id) is not None)
+ lambda m: re.search(args.match, m.id) is not None)
- if not opts.all:
+ if not args.all:
if command in ['apply']:
migrations = migrations.to_apply()
elif command in ['reapply', 'rollback']:
migrations = migrations.to_rollback()
- if not opts.batch:
+ if not args.batch:
migrations = prompt_migrations(conn, paramstyle, migrations, command)
- if not opts.batch and migrations:
+ if not args.batch and migrations:
if prompt(command.title() +
plural(len(migrations), " %d migration", " %d migrations") +
" to %s?" % dburi, "Yn") != 'y':
return 0
if command == 'reapply':
- migrations.rollback(opts.force)
- migrations.apply(opts.force)
+ migrations.rollback(args.force)
+ migrations.apply(args.force)
elif command == 'apply':
- migrations.apply(opts.force)
+ migrations.apply(args.force)
elif command == 'rollback':
- migrations.rollback(opts.force)
+ migrations.rollback(args.force)
if __name__ == "__main__":
main(sys.argv[1:])
diff --git a/yoyo/tests/__init__.py b/yoyo/tests/__init__.py
new file mode 100644
index 0000000..85e1ee7
--- /dev/null
+++ b/yoyo/tests/__init__.py
@@ -0,0 +1,38 @@
+from functools import wraps
+from tempfile import mkdtemp
+from shutil import rmtree
+import os.path
+import re
+
+dburi = "sqlite:///:memory:"
+
+
+def with_migrations(*migrations):
+ """
+ Decorator taking a list of migrations. Creates a temporary directory writes
+ each migration to a file (named '0.py', '1.py', '2.py' etc), calls the
+ decorated function with the directory name as the first argument, and
+ cleans up the temporary directory on exit.
+ """
+
+ def unindent(s):
+ initial_indent = re.search(r'^([ \t]*)\S', s, re.M).group(1)
+ return re.sub(r'(^|[\r\n]){0}'.format(re.escape(initial_indent)),
+ r'\1', s)
+
+ def decorator(func):
+ tmpdir = mkdtemp()
+ for ix, code in enumerate(migrations):
+ with open(os.path.join(tmpdir, '{0}.py'.format(ix)), 'w') as f:
+ f.write(unindent(code).strip())
+
+ @wraps(func)
+ def decorated(*args, **kwargs):
+ args = args + (tmpdir,)
+ try:
+ func(*args, **kwargs)
+ finally:
+ rmtree(tmpdir)
+
+ return decorated
+ return decorator
diff --git a/yoyo/tests/test_cli_script.py b/yoyo/tests/test_cli_script.py
new file mode 100644
index 0000000..9d4cc82
--- /dev/null
+++ b/yoyo/tests/test_cli_script.py
@@ -0,0 +1,71 @@
+import os.path
+
+from mock import patch, call
+
+from yoyo.tests import with_migrations, dburi
+from yoyo.scripts.migrate import main
+
+
+class TestYoyoScript(object):
+
+ def setup(self):
+ self.prompt_patch = patch('yoyo.scripts.migrate.prompt',
+ return_value='n')
+ self.prompt = self.prompt_patch.start()
+
+ def teardown(self):
+ self.prompt_patch.stop()
+
+ @with_migrations()
+ def test_it_sets_verbosity_level(self, tmpdir):
+ with patch('yoyo.scripts.migrate.configure_logging') as m:
+ main(['apply', tmpdir, dburi])
+ assert m.call_args == call(0)
+ main(['-vvv', 'apply', tmpdir, dburi])
+ assert m.call_args == call(3)
+
+ @with_migrations()
+ def test_it_prompts_to_cache_connection_params(self, tmpdir):
+ main(['apply', tmpdir, dburi])
+ assert 'save connection string' in self.prompt.call_args[0][0].lower()
+
+ @with_migrations()
+ def test_it_caches_connection_params(self, tmpdir):
+ self.prompt.return_value = 'y'
+ main(['apply', tmpdir, dburi])
+ assert os.path.exists(os.path.join(tmpdir, '.yoyo-migrate'))
+ with open(os.path.join(tmpdir, '.yoyo-migrate')) as f:
+ assert 'dburi = {0}'.format(dburi) in f.read()
+
+ @with_migrations()
+ def test_it_prompts_migrations(self, tmpdir):
+ with patch('yoyo.scripts.migrate.read_migrations') as read_migrations:
+ with patch('yoyo.scripts.migrate.prompt_migrations') \
+ as prompt_migrations:
+ main(['apply', tmpdir, dburi])
+ migrations = read_migrations().to_apply()
+ assert migrations in prompt_migrations.call_args[0]
+
+ @with_migrations()
+ def test_it_applies_migrations(self, tmpdir):
+ with patch('yoyo.scripts.migrate.read_migrations') as read_migrations:
+ main(['-b', 'apply', tmpdir, dburi])
+ migrations = read_migrations().to_apply()
+ assert migrations.rollback.call_count == 0
+ assert migrations.apply.call_count == 1
+
+ @with_migrations()
+ def test_it_rollsback_migrations(self, tmpdir):
+ with patch('yoyo.scripts.migrate.read_migrations') as read_migrations:
+ main(['-b', 'rollback', tmpdir, dburi])
+ migrations = read_migrations().to_rollback()
+ assert migrations.rollback.call_count == 1
+ assert migrations.apply.call_count == 0
+
+ @with_migrations()
+ def test_it_reapplies_migrations(self, tmpdir):
+ with patch('yoyo.scripts.migrate.read_migrations') as read_migrations:
+ main(['-b', 'reapply', tmpdir, dburi])
+ migrations = read_migrations().to_rollback()
+ assert migrations.rollback.call_count == 1
+ assert migrations.apply.call_count == 1
diff --git a/yoyo/tests.py b/yoyo/tests/test_migrations.py
index 5fb88f7..f125103 100644
--- a/yoyo/tests.py
+++ b/yoyo/tests/test_migrations.py
@@ -1,44 +1,8 @@
-import re
-from tempfile import mkdtemp
-from shutil import rmtree
-from functools import wraps
-import os
-
from yoyo.connections import connect
from yoyo import read_migrations
from yoyo import DatabaseError
-dburi = "sqlite:///:memory:"
-
-
-def with_migrations(*migrations):
- """
- Decorator taking a list of migrations. Creates a temporary directory writes
- each migration to a file (named '0.py', '1.py', '2.py' etc), calls the
- decorated function with the directory name as the first argument, and
- cleans up the temporary directory on exit.
- """
-
- def unindent(s):
- initial_indent = re.search(r'^([ \t]*)\S', s, re.M).group(1)
- return re.sub(r'(^|[\r\n]){0}'.format(re.escape(initial_indent)),
- r'\1', s)
-
- def decorator(func):
- tmpdir = mkdtemp()
- for ix, code in enumerate(migrations):
- with open(os.path.join(tmpdir, '{0}.py'.format(ix)), 'w') as f:
- f.write(unindent(code).strip())
-
- @wraps(func)
- def decorated(*args, **kwargs):
- try:
- func(tmpdir, *args, **kwargs)
- finally:
- rmtree(tmpdir)
-
- return decorated
- return decorator
+from yoyo.tests import with_migrations, dburi
@with_migrations(
@@ -155,7 +119,7 @@ def test_specify_migration_table(tmpdir):
migrations.apply()
cursor = conn.cursor()
cursor.execute("SELECT id FROM another_migration_table")
- assert cursor.fetchall() == [(u'0',)]
+ assert cursor.fetchall() == [('0',)]
@with_migrations(