update tests and docs for migrate.versioning.script.*
This commit is contained in:
parent
a626f5b1b9
commit
d44be57771
5
TODO
5
TODO
@ -3,3 +3,8 @@
|
||||
fail at test_changeset.test_fk(..))
|
||||
|
||||
- better SQL scripts support (testing, source viewing)
|
||||
|
||||
make_update_script_for_model:
|
||||
- calculated differences between models are actually differences between metas
|
||||
- columns are not compared?
|
||||
- even if two "models" are equal, it doesn't yield so
|
||||
|
17
docs/api.rst
17
docs/api.rst
@ -135,6 +135,23 @@ Module :mod:`shell <migrate.versioning.shell>`
|
||||
:members:
|
||||
:synopsis: Shell commands
|
||||
|
||||
Module :mod:`script <migrate.versioning.script>`
|
||||
------------------------------------------------
|
||||
|
||||
.. automodule:: migrate.versioning.script.base
|
||||
:synopsis: Script utilities
|
||||
:members:
|
||||
|
||||
.. automodule:: migrate.versioning.script.py
|
||||
:members:
|
||||
:inherited-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: migrate.versioning.script.sql
|
||||
:members:
|
||||
:show-inheritance:
|
||||
:inherited-members:
|
||||
|
||||
Module :mod:`util <migrate.versioning.util>`
|
||||
------------------------------------------------
|
||||
|
||||
|
@ -61,6 +61,7 @@ class Changeset(dict):
|
||||
|
||||
class Repository(pathed.Pathed):
|
||||
"""A project's change script repository"""
|
||||
|
||||
_config = 'migrate.cfg'
|
||||
_versions = 'versions'
|
||||
|
||||
@ -68,8 +69,8 @@ class Repository(pathed.Pathed):
|
||||
log.info('Loading repository %s...' % path)
|
||||
self.verify(path)
|
||||
super(Repository, self).__init__(path)
|
||||
self.config=cfgparse.Config(os.path.join(self.path, self._config))
|
||||
self.versions=version.Collection(os.path.join(self.path,
|
||||
self.config = cfgparse.Config(os.path.join(self.path, self._config))
|
||||
self.versions = version.Collection(os.path.join(self.path,
|
||||
self._versions))
|
||||
log.info('Repository %s loaded successfully' % path)
|
||||
log.debug('Config: %r' % self.config.to_dict())
|
||||
@ -116,6 +117,7 @@ class Repository(pathed.Pathed):
|
||||
tmplpkg = '.'.join((pkg, rsrc))
|
||||
tmplfile = resource_filename(pkg, rsrc)
|
||||
config_text = cls.prepare_config(tmplpkg, cls._config, name, **opts)
|
||||
|
||||
# Create repository
|
||||
try:
|
||||
shutil.copytree(tmplfile, path)
|
||||
@ -136,10 +138,17 @@ class Repository(pathed.Pathed):
|
||||
def create_script_sql(self, database, **k):
|
||||
self.versions.create_new_sql_version(database, **k)
|
||||
|
||||
latest=property(lambda self: self.versions.latest)
|
||||
version_table=property(lambda self: self.config.get('db_settings',
|
||||
'version_table'))
|
||||
id=property(lambda self: self.config.get('db_settings', 'repository_id'))
|
||||
@property
|
||||
def latest(self):
|
||||
return self.versions.latest
|
||||
|
||||
@property
|
||||
def version_table(self):
|
||||
return self.config.get('db_settings', 'version_table')
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.config.get('db_settings', 'repository_id')
|
||||
|
||||
def version(self, *p, **k):
|
||||
return self.versions.version(*p, **k)
|
||||
@ -177,7 +186,7 @@ def manage(file, **opts):
|
||||
pkg, rsrc = template.manage(as_pkg=True)
|
||||
tmpl = resource_string(pkg, rsrc)
|
||||
vars = ",".join(["%s='%s'" % vars for vars in opts.iteritems()])
|
||||
result = tmpl%dict(defaults=vars)
|
||||
result = tmpl % dict(defaults=vars)
|
||||
|
||||
fd = open(file, 'w')
|
||||
fd.write(result)
|
||||
|
@ -1,3 +1,6 @@
|
||||
from py import PythonScript
|
||||
from sql import SqlScript
|
||||
from base import BaseScript
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from migrate.versioning.script.base import BaseScript
|
||||
from migrate.versioning.script.py import PythonScript
|
||||
from migrate.versioning.script.sql import SqlScript
|
||||
|
@ -1,12 +1,12 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from migrate.versioning.base import log,operations
|
||||
from migrate.versioning import pathed,exceptions
|
||||
from migrate.versioning.base import log, operations
|
||||
from migrate.versioning import pathed, exceptions
|
||||
|
||||
|
||||
class BaseScript(pathed.Pathed):
|
||||
"""Base class for other types of scripts
|
||||
"""Base class for other types of scripts.
|
||||
All scripts have the following properties:
|
||||
|
||||
source (script.source())
|
||||
@ -17,18 +17,20 @@ class BaseScript(pathed.Pathed):
|
||||
The operations defined by the script: upgrade(), downgrade() or both.
|
||||
Returns a tuple of operations.
|
||||
Can also check for an operation with ex. script.operation(Script.ops.up)
|
||||
"""
|
||||
""" # TODO: sphinxfy this and implement it correctly
|
||||
|
||||
def __init__(self,path):
|
||||
def __init__(self, path):
|
||||
log.info('Loading script %s...' % path)
|
||||
self.verify(path)
|
||||
super(BaseScript, self).__init__(path)
|
||||
log.info('Script %s loaded successfully' % path)
|
||||
|
||||
@classmethod
|
||||
def verify(cls,path):
|
||||
"""Ensure this is a valid script, or raise InvalidScriptError
|
||||
def verify(cls, path):
|
||||
"""Ensure this is a valid script
|
||||
This version simply ensures the script file's existence
|
||||
|
||||
:raises: :exc:`InvalidScriptError <migrate.versioning.exceptions.InvalidScriptError>`
|
||||
"""
|
||||
try:
|
||||
cls.require_found(path)
|
||||
@ -36,10 +38,16 @@ class BaseScript(pathed.Pathed):
|
||||
raise exceptions.InvalidScriptError(path)
|
||||
|
||||
def source(self):
|
||||
""":returns: source code of the script.
|
||||
:rtype: string
|
||||
"""
|
||||
fd = open(self.path)
|
||||
ret = fd.read()
|
||||
fd.close()
|
||||
return ret
|
||||
|
||||
def run(self, engine):
|
||||
"""Core of each BaseScript subclass.
|
||||
This method executes the script.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
@ -12,10 +12,13 @@ from migrate.versioning.script import base
|
||||
from migrate.versioning.util import import_path, load_model, construct_engine
|
||||
|
||||
class PythonScript(base.BaseScript):
|
||||
"""Base for Python scripts"""
|
||||
|
||||
@classmethod
|
||||
def create(cls, path, **opts):
|
||||
"""Create an empty migration script"""
|
||||
"""Create an empty migration script at specified path
|
||||
|
||||
:returns: :class:`PythonScript instance <migrate.versioning.script.py.PythonScript>`"""
|
||||
cls.require_notfound(path)
|
||||
|
||||
# TODO: Use the default script template (defined in the template
|
||||
@ -25,30 +28,51 @@ class PythonScript(base.BaseScript):
|
||||
src = template.get_script(template_file)
|
||||
shutil.copy(src, path)
|
||||
|
||||
return cls(path)
|
||||
|
||||
@classmethod
|
||||
def make_update_script_for_model(cls, engine, oldmodel,
|
||||
model, repository, **opts):
|
||||
"""Create a migration script"""
|
||||
"""Create a migration script based on difference between two SA models.
|
||||
|
||||
:param repository: path to migrate repository
|
||||
:param oldmodel: dotted.module.name:SAClass or SAClass object
|
||||
:param model: dotted.module.name:SAClass or SAClass object
|
||||
:param engine: SQLAlchemy engine
|
||||
:type repository: string or :class:`Repository instance <migrate.versioning.repository.Repository>`
|
||||
:type oldmodel: string or Class
|
||||
:type model: string or Class
|
||||
:type engine: Engine instance
|
||||
:returns: Upgrade / Downgrade script
|
||||
:rtype: string
|
||||
"""
|
||||
|
||||
# Compute differences.
|
||||
if isinstance(repository, basestring):
|
||||
# oh dear, an import cycle!
|
||||
from migrate.versioning.repository import Repository
|
||||
repository = Repository(repository)
|
||||
|
||||
oldmodel = load_model(oldmodel)
|
||||
model = load_model(model)
|
||||
|
||||
# Compute differences.
|
||||
diff = schemadiff.getDiffOfModelAgainstModel(
|
||||
oldmodel,
|
||||
model,
|
||||
engine,
|
||||
excludeTables=[repository.version_table])
|
||||
# TODO: diff can be False (there is no difference?)
|
||||
decls, upgradeCommands, downgradeCommands = \
|
||||
genmodel.ModelGenerator(diff).toUpgradeDowngradePython()
|
||||
|
||||
# Store differences into file.
|
||||
template_file = None
|
||||
src = template.get_script(template_file)
|
||||
contents = open(src).read()
|
||||
# TODO: add custom templates
|
||||
src = template.get_script(None)
|
||||
f = open(src)
|
||||
contents = f.read()
|
||||
f.close()
|
||||
|
||||
# generate source
|
||||
search = 'def upgrade():'
|
||||
contents = contents.replace(search, '\n\n'.join((decls, search)), 1)
|
||||
if upgradeCommands:
|
||||
@ -58,11 +82,18 @@ class PythonScript(base.BaseScript):
|
||||
return contents
|
||||
|
||||
@classmethod
|
||||
def verify_module(cls,path):
|
||||
"""Ensure this is a valid script, or raise InvalidScriptError"""
|
||||
def verify_module(cls, path):
|
||||
"""Ensure path is a valid script
|
||||
|
||||
:param path: Script location
|
||||
:type path: string
|
||||
|
||||
:raises: :exc:`InvalidScriptError <migrate.versioning.exceptions.InvalidScriptError>`
|
||||
:returns: Python module
|
||||
"""
|
||||
# Try to import and get the upgrade() func
|
||||
try:
|
||||
module=import_path(path)
|
||||
module = import_path(path)
|
||||
except:
|
||||
# If the script itself has errors, that's not our problem
|
||||
raise
|
||||
@ -73,8 +104,11 @@ class PythonScript(base.BaseScript):
|
||||
return module
|
||||
|
||||
def preview_sql(self, url, step, **args):
|
||||
"""Mock engine to store all executable calls in a string \
|
||||
and execute the step"""
|
||||
"""Mocks SQLAlchemy Engine to store all executed calls in a string
|
||||
and runs :meth:`PythonScript.run <migrate.versioning.script.py.PythonScript.run>`
|
||||
|
||||
:returns: SQL file
|
||||
"""
|
||||
buf = StringIO()
|
||||
args['engine_arg_strategy'] = 'mock'
|
||||
args['engine_arg_executor'] = lambda s, p='': buf.write(s + p)
|
||||
@ -85,8 +119,14 @@ class PythonScript(base.BaseScript):
|
||||
return buf.getvalue()
|
||||
|
||||
def run(self, engine, step):
|
||||
"""Core method of Script file. \
|
||||
Exectues update() or downgrade() function"""
|
||||
"""Core method of Script file.
|
||||
Exectues :func:`update` or :func:`downgrade` functions
|
||||
|
||||
:param engine: SQLAlchemy Engine
|
||||
:param step: Operation to run
|
||||
:type engine: string
|
||||
:type step: int
|
||||
"""
|
||||
if step > 0:
|
||||
op = 'upgrade'
|
||||
elif step < 0:
|
||||
@ -104,13 +144,16 @@ class PythonScript(base.BaseScript):
|
||||
|
||||
@property
|
||||
def module(self):
|
||||
if not hasattr(self,'_module'):
|
||||
"""Calls :meth:`migrate.versioning.script.py.verify_module`
|
||||
and returns it.
|
||||
"""
|
||||
if not hasattr(self, '_module'):
|
||||
self._module = self.verify_module(self.path)
|
||||
return self._module
|
||||
|
||||
def _func(self, funcname):
|
||||
fn = getattr(self.module, funcname, None)
|
||||
if not fn:
|
||||
try:
|
||||
return getattr(self.module, funcname)
|
||||
except AttributeError:
|
||||
msg = "The function %s is not defined in this script"
|
||||
raise exceptions.ScriptError(msg%funcname)
|
||||
return fn
|
||||
raise exceptions.ScriptError(msg % funcname)
|
||||
|
@ -1,8 +1,15 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from migrate.versioning.script import base
|
||||
|
||||
|
||||
class SqlScript(base.BaseScript):
|
||||
"""A file containing plain SQL statements."""
|
||||
def run(self, engine, step):
|
||||
|
||||
# TODO: why is step parameter even here?
|
||||
def run(self, engine, step=None):
|
||||
"""Runs SQL script through raw dbapi execute call"""
|
||||
text = self.source()
|
||||
# Don't rely on SA's autocommit here
|
||||
# (SA uses .startswith to check if a commit is needed. What if script
|
||||
@ -11,14 +18,13 @@ class SqlScript(base.BaseScript):
|
||||
try:
|
||||
trans = conn.begin()
|
||||
try:
|
||||
# ###HACK: SQLite doesn't allow multiple statements through
|
||||
# HACK: SQLite doesn't allow multiple statements through
|
||||
# its execute() method, but it provides executescript() instead
|
||||
dbapi = conn.engine.raw_connection()
|
||||
if getattr(dbapi, 'executescript', None):
|
||||
dbapi.executescript(text)
|
||||
else:
|
||||
conn.execute(text)
|
||||
# Success
|
||||
trans.commit()
|
||||
except:
|
||||
trans.rollback()
|
||||
|
@ -91,8 +91,13 @@ def construct_engine(url, **opts):
|
||||
|
||||
Currently, there are 2 ways to pass create_engine options to :mod:`migrate.versioning.api` functions:
|
||||
|
||||
:param url: connection string
|
||||
:param engine_dict: python dictionary of options to pass to `create_engine`
|
||||
:param engine_arg_*: keyword parameters to pass to `create_engine` (evaluated with :func:`migrate.versioning.util.guess_obj_type`)
|
||||
:type engine_dict: dict
|
||||
:type url: string
|
||||
:type engine_arg_*: string
|
||||
:returns: SQLAlchemy Engine
|
||||
|
||||
.. note::
|
||||
|
||||
|
@ -7,8 +7,8 @@ tag_svn_revision = 1
|
||||
tag_build = .dev
|
||||
|
||||
[nosetests]
|
||||
#pdb = true
|
||||
#pdb-failures = true
|
||||
pdb = true
|
||||
pdb-failures = true
|
||||
|
||||
[aliases]
|
||||
release = egg_info -RDb ''
|
||||
|
@ -2,6 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
@ -16,10 +17,12 @@ class Pathed(base.Base):
|
||||
def setUp(self):
|
||||
super(Pathed, self).setUp()
|
||||
self.temp_usable_dir = tempfile.mkdtemp()
|
||||
sys.path.append(self.temp_usable_dir)
|
||||
|
||||
def tearDown(self):
|
||||
super(Pathed, self).tearDown()
|
||||
self.temp_usable_dir = tempfile.mkdtemp()
|
||||
sys.path.remove(self.temp_usable_dir)
|
||||
Pathed.purge(self.temp_usable_dir)
|
||||
|
||||
@classmethod
|
||||
def _tmp(cls, prefix='', suffix=''):
|
||||
|
@ -2,13 +2,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
from migrate.versioning import exceptions, version, repository
|
||||
from migrate.versioning.script import *
|
||||
from migrate.versioning import exceptions, version
|
||||
from migrate.versioning.util import *
|
||||
|
||||
from test import fixture
|
||||
|
||||
|
||||
class TestBaseScript(fixture.Pathed):
|
||||
|
||||
def test_all(self):
|
||||
"""Testing all basic BaseScript operations"""
|
||||
# verify / source / run
|
||||
src = self.tmp()
|
||||
open(src, 'w').close()
|
||||
bscript = BaseScript(src)
|
||||
BaseScript.verify(src)
|
||||
self.assertEqual(bscript.source(), '')
|
||||
self.assertRaises(NotImplementedError, bscript.run, 'foobar')
|
||||
|
||||
|
||||
class TestPyScript(fixture.Pathed, fixture.DB):
|
||||
cls = PythonScript
|
||||
def test_create(self):
|
||||
@ -22,6 +38,16 @@ class TestPyScript(fixture.Pathed, fixture.DB):
|
||||
# Can't create it again: it already exists
|
||||
self.assertRaises(exceptions.PathFoundError,self.cls.create,path)
|
||||
|
||||
@fixture.usedb(supported='sqlite')
|
||||
def test_run(self):
|
||||
script_path = self.tmp_py()
|
||||
pyscript = PythonScript.create(script_path)
|
||||
pyscript.run(self.engine, 1)
|
||||
pyscript.run(self.engine, -1)
|
||||
|
||||
self.assertRaises(exceptions.ScriptError, pyscript.run, self.engine, 0)
|
||||
self.assertRaises(exceptions.ScriptError, pyscript._func, 'foobar')
|
||||
|
||||
def test_verify_notfound(self):
|
||||
"""Correctly verify a python migration script: nonexistant file"""
|
||||
path = self.tmp_py()
|
||||
@ -93,7 +119,80 @@ def upgrade():
|
||||
# Succeeds after creating
|
||||
self.cls.create(path)
|
||||
self.cls.verify(path)
|
||||
|
||||
class TestSqlScript(fixture.Pathed):
|
||||
pass
|
||||
|
||||
|
||||
# test for PythonScript.make_update_script_for_model
|
||||
|
||||
@fixture.usedb()
|
||||
def test_make_update_script_for_model(self):
|
||||
"""Construct script source from differences of two models"""
|
||||
|
||||
self.setup_model_params()
|
||||
self.write_file(self.first_model_path, self.base_source)
|
||||
self.write_file(self.second_model_path, self.base_source + self.model_source)
|
||||
|
||||
|
||||
source_script = self.pyscript.make_update_script_for_model(
|
||||
engine=self.engine,
|
||||
oldmodel=load_model('testmodel_first:meta'),
|
||||
model=load_model('testmodel_second:meta'),
|
||||
repository=self.repo_path,
|
||||
)
|
||||
|
||||
self.assertTrue('User.create()' in source_script)
|
||||
self.assertTrue('User.drop()' in source_script)
|
||||
|
||||
#@fixture.usedb()
|
||||
#def test_make_update_script_for_model_equals(self):
|
||||
# """Try to make update script from two identical models"""
|
||||
|
||||
# self.setup_model_params()
|
||||
# self.write_file(self.first_model_path, self.base_source + self.model_source)
|
||||
# self.write_file(self.second_model_path, self.base_source + self.model_source)
|
||||
|
||||
# source_script = self.pyscript.make_update_script_for_model(
|
||||
# engine=self.engine,
|
||||
# oldmodel=load_model('testmodel_first:meta'),
|
||||
# model=load_model('testmodel_second:meta'),
|
||||
# repository=self.repo_path,
|
||||
# )
|
||||
|
||||
# self.assertFalse('User.create()' in source_script)
|
||||
# self.assertFalse('User.drop()' in source_script)
|
||||
|
||||
def setup_model_params(self):
|
||||
self.script_path = self.tmp_py()
|
||||
self.repo_path = self.tmp()
|
||||
self.first_model_path = os.path.join(self.temp_usable_dir, 'testmodel_first.py')
|
||||
self.second_model_path = os.path.join(self.temp_usable_dir, 'testmodel_second.py')
|
||||
|
||||
self.base_source = """from sqlalchemy import *\nmeta = MetaData()\n"""
|
||||
self.model_source = """
|
||||
User = Table('User', meta,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('login', Unicode(40)),
|
||||
Column('passwd', String(40)),
|
||||
)"""
|
||||
|
||||
self.repo = repository.Repository.create(self.repo_path, 'repo')
|
||||
self.pyscript = PythonScript.create(self.script_path)
|
||||
|
||||
|
||||
def write_file(self, path, contents):
|
||||
f = open(path, 'w')
|
||||
f.write(contents)
|
||||
f.close()
|
||||
|
||||
|
||||
class TestSqlScript(fixture.Pathed, fixture.DB):
|
||||
|
||||
@fixture.usedb()
|
||||
def test_error(self):
|
||||
"""Test if exception is raised on wrong script source"""
|
||||
src = self.tmp()
|
||||
|
||||
f = open(src, 'w')
|
||||
f.write("""foobar""")
|
||||
f.close()
|
||||
|
||||
sqls = SqlScript(src)
|
||||
self.assertRaises(Exception, sqls.run, self.engine)
|
||||
|
@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
|
||||
from test import fixture
|
||||
from migrate.versioning.util import *
|
||||
|
||||
@ -51,16 +53,16 @@ class TestUtil(fixture.Pathed):
|
||||
|
||||
def test_load_model(self):
|
||||
"""load model from dotted name"""
|
||||
model_path = self.tmp_named('testmodel.py')
|
||||
model_path = os.path.join(self.temp_usable_dir, 'test_load_model.py')
|
||||
|
||||
f = open(model_path, 'w')
|
||||
f.write("class FakeFloat(int): pass")
|
||||
f.close()
|
||||
|
||||
FakeFloat = load_model('testmodel.FakeFloat')
|
||||
FakeFloat = load_model('test_load_model.FakeFloat')
|
||||
self.assert_(isinstance(FakeFloat(), int))
|
||||
|
||||
FakeFloat = load_model('testmodel:FakeFloat')
|
||||
FakeFloat = load_model('test_load_model:FakeFloat')
|
||||
self.assert_(isinstance(FakeFloat(), int))
|
||||
|
||||
FakeFloat = load_model(FakeFloat)
|
||||
|
Loading…
x
Reference in New Issue
Block a user