add option to customize templates and use multiple themes
This commit is contained in:
parent
7cb4b6363c
commit
78ce747e25
@ -1,6 +1,7 @@
|
|||||||
0.5.5
|
0.5.5
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
- added option to define custom templates through option ``--templates_path``, read more in :ref:`tutorial section <custom-templates>`
|
||||||
- url parameter can also be an Engine instance (this usage is discouraged though sometimes necessary)
|
- url parameter can also be an Engine instance (this usage is discouraged though sometimes necessary)
|
||||||
- added support for SQLAlchemy 0.6 (missing oracle and firebird) by Michael Bayer
|
- added support for SQLAlchemy 0.6 (missing oracle and firebird) by Michael Bayer
|
||||||
- alter, create, drop column / rename table / rename index constructs now accept `alter_metadata` parameter. If True, it will modify Column/Table objects according to changes. Otherwise, everything will be untouched.
|
- alter, create, drop column / rename table / rename index constructs now accept `alter_metadata` parameter. If True, it will modify Column/Table objects according to changes. Otherwise, everything will be untouched.
|
||||||
|
@ -493,3 +493,25 @@ currently:
|
|||||||
the databases your application will actually be using to ensure your
|
the databases your application will actually be using to ensure your
|
||||||
updates to that database work properly. This must be a list;
|
updates to that database work properly. This must be a list;
|
||||||
example: `['postgres', 'sqlite']`
|
example: `['postgres', 'sqlite']`
|
||||||
|
|
||||||
|
|
||||||
|
.. _custom-templates:
|
||||||
|
|
||||||
|
Customize templates
|
||||||
|
===================
|
||||||
|
|
||||||
|
Users can pass ``templates_path`` to API functions to provide customized templates path.
|
||||||
|
Path should be a collection of templates, like ``migrate.versioning.templates`` package directory.
|
||||||
|
|
||||||
|
One may also want to specify custom themes. API functions accept ``templates_theme`` for this purpose (which defaults to `default`)
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
/home/user/templates/manage $ ls
|
||||||
|
default.py_tmpl
|
||||||
|
pylons.py_tmpl
|
||||||
|
|
||||||
|
/home/user/templates/manage $ migrate manage manage.py --templates_path=/home/user/templates --templates_theme=pylons
|
||||||
|
|
||||||
|
|
||||||
|
.. versionadded:: 0.6.0
|
||||||
|
@ -264,7 +264,7 @@ def manage(file, **opts):
|
|||||||
python manage.py version
|
python manage.py version
|
||||||
%prog version --repository=/path/to/repository
|
%prog version --repository=/path/to/repository
|
||||||
"""
|
"""
|
||||||
return Repository.create_manage_file(file, **opts)
|
Repository.create_manage_file(file, **opts)
|
||||||
|
|
||||||
|
|
||||||
def compare_model_to_db(url, model, repository, **opts):
|
def compare_model_to_db(url, model, repository, **opts):
|
||||||
|
@ -4,10 +4,10 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import string
|
import string
|
||||||
from pkg_resources import resource_string, resource_filename
|
from pkg_resources import resource_filename
|
||||||
|
|
||||||
from migrate.versioning import exceptions, script, version, pathed, cfgparse
|
from migrate.versioning import exceptions, script, version, pathed, cfgparse
|
||||||
from migrate.versioning.template import template
|
from migrate.versioning.template import Template
|
||||||
from migrate.versioning.base import *
|
from migrate.versioning.base import *
|
||||||
|
|
||||||
|
|
||||||
@ -91,11 +91,18 @@ class Repository(pathed.Pathed):
|
|||||||
except exceptions.PathNotFoundError, e:
|
except exceptions.PathNotFoundError, e:
|
||||||
raise exceptions.InvalidRepositoryError(path)
|
raise exceptions.InvalidRepositoryError(path)
|
||||||
|
|
||||||
# TODO: what are those options?
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def prepare_config(cls, pkg, rsrc, name, **opts):
|
def prepare_config(cls, tmpl_dir, config_file, name, **opts):
|
||||||
"""
|
"""
|
||||||
Prepare a project configuration file for a new project.
|
Prepare a project configuration file for a new project.
|
||||||
|
|
||||||
|
:param tmpl_dir: Path to Repository template
|
||||||
|
:param config_file: Name of the config file in Repository template
|
||||||
|
:param name: Repository name
|
||||||
|
:type tmpl_dir: string
|
||||||
|
:type config_file: string
|
||||||
|
:type name: string
|
||||||
|
:returns: Populated config file
|
||||||
"""
|
"""
|
||||||
# Prepare opts
|
# Prepare opts
|
||||||
defaults = dict(
|
defaults = dict(
|
||||||
@ -105,7 +112,7 @@ class Repository(pathed.Pathed):
|
|||||||
|
|
||||||
defaults.update(opts)
|
defaults.update(opts)
|
||||||
|
|
||||||
tmpl = resource_string(pkg, rsrc)
|
tmpl = open(os.path.join(tmpl_dir, config_file)).read()
|
||||||
ret = string.Template(tmpl).substitute(defaults)
|
ret = string.Template(tmpl).substitute(defaults)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@ -113,14 +120,12 @@ class Repository(pathed.Pathed):
|
|||||||
def create(cls, path, name, **opts):
|
def create(cls, path, name, **opts):
|
||||||
"""Create a repository at a specified path"""
|
"""Create a repository at a specified path"""
|
||||||
cls.require_notfound(path)
|
cls.require_notfound(path)
|
||||||
|
theme = opts.get('templates_theme', None)
|
||||||
pkg, rsrc = template.get_repository(as_pkg=True)
|
|
||||||
tmplpkg = '.'.join((pkg, rsrc))
|
|
||||||
tmplfile = resource_filename(pkg, rsrc)
|
|
||||||
config_text = cls.prepare_config(tmplpkg, cls._config, name, **opts)
|
|
||||||
|
|
||||||
# Create repository
|
# Create repository
|
||||||
shutil.copytree(tmplfile, path)
|
tmpl_dir = Template(opts.pop('templates_path', None)).get_repository(theme=theme)
|
||||||
|
config_text = cls.prepare_config(tmpl_dir, cls._config, name, **opts)
|
||||||
|
shutil.copytree(tmpl_dir, path)
|
||||||
|
|
||||||
# Edit config defaults
|
# Edit config defaults
|
||||||
fd = open(os.path.join(path, cls._config), 'w')
|
fd = open(os.path.join(path, cls._config), 'w')
|
||||||
@ -129,7 +134,7 @@ class Repository(pathed.Pathed):
|
|||||||
|
|
||||||
# Create a management script
|
# Create a management script
|
||||||
manager = os.path.join(path, 'manage.py')
|
manager = os.path.join(path, 'manage.py')
|
||||||
Repository.create_manage_file(manager, repository=path)
|
Repository.create_manage_file(manager, theme=theme, repository=path)
|
||||||
|
|
||||||
return cls(path)
|
return cls(path)
|
||||||
|
|
||||||
@ -205,12 +210,10 @@ class Repository(pathed.Pathed):
|
|||||||
:param file_: Destination file to be written
|
:param file_: Destination file to be written
|
||||||
:param opts: Options that are passed to template
|
:param opts: Options that are passed to template
|
||||||
"""
|
"""
|
||||||
|
mng_file = Template(opts.pop('templates_path', None)).get_manage(theme=opts.pop('templates_theme', None))
|
||||||
vars_ = ",".join(["%s='%s'" % var for var in opts.iteritems()])
|
vars_ = ",".join(["%s='%s'" % var for var in opts.iteritems()])
|
||||||
|
|
||||||
pkg, rsrc = template.manage(as_pkg=True)
|
tmpl = open(mng_file).read()
|
||||||
tmpl = resource_string(pkg, rsrc)
|
|
||||||
result = tmpl % dict(defaults=vars_)
|
|
||||||
|
|
||||||
fd = open(file_, 'w')
|
fd = open(file_, 'w')
|
||||||
fd.write(result)
|
fd.write(tmpl % dict(defaults=vars_))
|
||||||
fd.close()
|
fd.close()
|
||||||
|
@ -7,7 +7,7 @@ from StringIO import StringIO
|
|||||||
import migrate
|
import migrate
|
||||||
from migrate.versioning import exceptions, genmodel, schemadiff
|
from migrate.versioning import exceptions, genmodel, schemadiff
|
||||||
from migrate.versioning.base import operations
|
from migrate.versioning.base import operations
|
||||||
from migrate.versioning.template import template
|
from migrate.versioning.template import Template
|
||||||
from migrate.versioning.script import base
|
from migrate.versioning.script import base
|
||||||
from migrate.versioning.util import import_path, load_model, construct_engine
|
from migrate.versioning.util import import_path, load_model, construct_engine
|
||||||
|
|
||||||
@ -22,11 +22,7 @@ class PythonScript(base.BaseScript):
|
|||||||
:returns: :class:`PythonScript instance <migrate.versioning.script.py.PythonScript>`"""
|
:returns: :class:`PythonScript instance <migrate.versioning.script.py.PythonScript>`"""
|
||||||
cls.require_notfound(path)
|
cls.require_notfound(path)
|
||||||
|
|
||||||
# TODO: Use the default script template (defined in the template
|
src = Template(opts.pop('templates_path', None)).get_script(theme=opts.pop('templates_theme', None))
|
||||||
# module) for now, but we might want to allow people to specify a
|
|
||||||
# different one later.
|
|
||||||
template_file = None
|
|
||||||
src = template.get_script(template_file)
|
|
||||||
shutil.copy(src, path)
|
shutil.copy(src, path)
|
||||||
|
|
||||||
return cls(path)
|
return cls(path)
|
||||||
@ -67,8 +63,7 @@ class PythonScript(base.BaseScript):
|
|||||||
genmodel.ModelGenerator(diff).toUpgradeDowngradePython()
|
genmodel.ModelGenerator(diff).toUpgradeDowngradePython()
|
||||||
|
|
||||||
# Store differences into file.
|
# Store differences into file.
|
||||||
# TODO: add custom templates
|
src = Template(opts.pop('templates_path', None)).get_script(opts.pop('templates_theme', None))
|
||||||
src = template.get_script(None)
|
|
||||||
f = open(src)
|
f = open(src)
|
||||||
contents = f.read()
|
contents = f.read()
|
||||||
f.close()
|
f.close()
|
||||||
|
@ -4,81 +4,84 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from pkg_resources import resource_filename
|
from pkg_resources import resource_filename
|
||||||
|
|
||||||
from migrate.versioning.base import *
|
from migrate.versioning.base import *
|
||||||
from migrate.versioning import pathed
|
from migrate.versioning import pathed
|
||||||
|
|
||||||
|
|
||||||
class Packaged(pathed.Pathed):
|
class Collection(pathed.Pathed):
|
||||||
"""An object assoc'ed with a Python package"""
|
|
||||||
|
|
||||||
def __init__(self, pkg):
|
|
||||||
self.pkg = pkg
|
|
||||||
path = self._find_path(pkg)
|
|
||||||
super(Packaged, self).__init__(path)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _find_path(cls, pkg):
|
|
||||||
pkg_name, resource_name = pkg.rsplit('.', 1)
|
|
||||||
ret = resource_filename(pkg_name, resource_name)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
class Collection(Packaged):
|
|
||||||
"""A collection of templates of a specific type"""
|
"""A collection of templates of a specific type"""
|
||||||
|
_mask = None
|
||||||
_default = None
|
|
||||||
|
|
||||||
def get_path(self, file):
|
def get_path(self, file):
|
||||||
return os.path.join(self.path, str(file))
|
return os.path.join(self.path, str(file))
|
||||||
|
|
||||||
def get_pkg(self, file):
|
|
||||||
return (self.pkg, str(file))
|
|
||||||
|
|
||||||
|
|
||||||
class RepositoryCollection(Collection):
|
class RepositoryCollection(Collection):
|
||||||
_default = 'default'
|
_mask = '%s'
|
||||||
|
|
||||||
|
|
||||||
class ScriptCollection(Collection):
|
class ScriptCollection(Collection):
|
||||||
_default = 'default.py_tmpl'
|
_mask = '%s.py_tmpl'
|
||||||
|
|
||||||
|
class ManageCollection(Collection):
|
||||||
|
_mask = '%s.py_tmpl'
|
||||||
|
|
||||||
|
|
||||||
class Template(Packaged):
|
class Template(pathed.Pathed):
|
||||||
"""Finds the paths/packages of various Migrate templates"""
|
"""Finds the paths/packages of various Migrate templates.
|
||||||
|
|
||||||
_repository = 'repository'
|
:param path: Templates are loaded from migrate package
|
||||||
_script = 'script'
|
if `path` is not provided.
|
||||||
|
"""
|
||||||
|
pkg = 'migrate.versioning.templates'
|
||||||
_manage = 'manage.py_tmpl'
|
_manage = 'manage.py_tmpl'
|
||||||
|
|
||||||
def __init__(self, pkg):
|
def __new__(cls, path=None):
|
||||||
super(Template, self).__init__(pkg)
|
if path is None:
|
||||||
self.repository = RepositoryCollection('.'.join((self.pkg,
|
path = cls._find_path(cls.pkg)
|
||||||
self._repository)))
|
return super(Template, cls).__new__(cls, path)
|
||||||
self.script = ScriptCollection('.'.join((self.pkg, self._script)))
|
|
||||||
|
|
||||||
def get_item(self, attr, filename=None, as_pkg=None, as_str=None):
|
def __init__(self, path=None):
|
||||||
item = getattr(self, attr)
|
if path is None:
|
||||||
if filename is None:
|
path = Template._find_path(self.pkg)
|
||||||
filename = getattr(item, '_default')
|
super(Template, self).__init__(path)
|
||||||
if as_pkg:
|
self.repository = RepositoryCollection(os.path.join(path, 'repository'))
|
||||||
ret = item.get_pkg(filename)
|
self.script = ScriptCollection(os.path.join(path, 'script'))
|
||||||
if as_str:
|
self.manage = ManageCollection(os.path.join(path, 'manage'))
|
||||||
ret = '.'.join(ret)
|
|
||||||
|
@classmethod
|
||||||
|
def _find_path(cls, pkg):
|
||||||
|
"""Returns absolute path to dotted python package."""
|
||||||
|
tmp_pkg = pkg.rsplit('.', 1)
|
||||||
|
|
||||||
|
if len(tmp_pkg) != 1:
|
||||||
|
return resource_filename(tmp_pkg[0], tmp_pkg[1])
|
||||||
else:
|
else:
|
||||||
ret = item.get_path(filename)
|
return resource_filename(tmp_pkg[0], '')
|
||||||
return ret
|
|
||||||
|
|
||||||
def get_repository(self, filename=None, as_pkg=None, as_str=None):
|
def _get_item(self, collection, theme=None):
|
||||||
return self.get_item('repository', filename, as_pkg, as_str)
|
"""Locates and returns collection.
|
||||||
|
|
||||||
|
:param collection: name of collection to locate
|
||||||
|
:param type_: type of subfolder in collection (defaults to "_default")
|
||||||
|
:returns: (package, source)
|
||||||
|
:rtype: str, str
|
||||||
|
"""
|
||||||
|
item = getattr(self, collection)
|
||||||
|
theme_mask = getattr(item, '_mask')
|
||||||
|
theme = theme_mask % (theme or 'default')
|
||||||
|
return item.get_path(theme)
|
||||||
|
|
||||||
|
def get_repository(self, *a, **kw):
|
||||||
|
"""Calls self._get_item('repository', *a, **kw)"""
|
||||||
|
return self._get_item('repository', *a, **kw)
|
||||||
|
|
||||||
def get_script(self, filename=None, as_pkg=None, as_str=None):
|
def get_script(self, *a, **kw):
|
||||||
return self.get_item('script', filename, as_pkg, as_str)
|
"""Calls self._get_item('script', *a, **kw)"""
|
||||||
|
return self._get_item('script', *a, **kw)
|
||||||
|
|
||||||
def manage(self, **k):
|
def get_manage(self, *a, **kw):
|
||||||
return (self.pkg, self._manage)
|
"""Calls self._get_item('manage', *a, **kw)"""
|
||||||
|
return self._get_item('manage', *a, **kw)
|
||||||
|
|
||||||
template_pkg = 'migrate.versioning.templates'
|
|
||||||
template = Template(template_pkg)
|
|
||||||
|
@ -101,7 +101,7 @@ class Collection(pathed.Pathed):
|
|||||||
if os.path.exists(filepath):
|
if os.path.exists(filepath):
|
||||||
raise Exception('Script already exists: %s' % filepath)
|
raise Exception('Script already exists: %s' % filepath)
|
||||||
else:
|
else:
|
||||||
script.PythonScript.create(filepath)
|
script.PythonScript.create(filepath, **k)
|
||||||
|
|
||||||
self.versions[ver] = Version(ver, self.path, [filename])
|
self.versions[ver] = Version(ver, self.path, [filename])
|
||||||
|
|
||||||
|
@ -1,21 +1,27 @@
|
|||||||
from test import fixture
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from migrate.versioning import cfgparse
|
from migrate.versioning import cfgparse
|
||||||
from migrate.versioning.repository import *
|
from migrate.versioning.repository import *
|
||||||
|
from migrate.versioning.template import Template
|
||||||
|
from test import fixture
|
||||||
|
|
||||||
|
|
||||||
class TestConfigParser(fixture.Base):
|
class TestConfigParser(fixture.Base):
|
||||||
|
|
||||||
def test_to_dict(self):
|
def test_to_dict(self):
|
||||||
"""Correctly interpret config results as dictionaries"""
|
"""Correctly interpret config results as dictionaries"""
|
||||||
parser = cfgparse.Parser(dict(default_value=42))
|
parser = cfgparse.Parser(dict(default_value=42))
|
||||||
self.assert_(len(parser.sections())==0)
|
self.assert_(len(parser.sections()) == 0)
|
||||||
parser.add_section('section')
|
parser.add_section('section')
|
||||||
parser.set('section','option','value')
|
parser.set('section','option','value')
|
||||||
self.assert_(parser.get('section','option')=='value')
|
self.assertEqual(parser.get('section', 'option'), 'value')
|
||||||
self.assert_(parser.to_dict()['section']['option']=='value')
|
self.assertEqual(parser.to_dict()['section']['option'], 'value')
|
||||||
|
|
||||||
def test_table_config(self):
|
def test_table_config(self):
|
||||||
"""We should be able to specify the table to be used with a repository"""
|
"""We should be able to specify the table to be used with a repository"""
|
||||||
default_text=Repository.prepare_config(template.get_repository(as_pkg=True,as_str=True),
|
default_text = Repository.prepare_config(Template().get_repository(),
|
||||||
Repository._config,'repository_name')
|
Repository._config, 'repository_name')
|
||||||
specified_text=Repository.prepare_config(template.get_repository(as_pkg=True,as_str=True),
|
specified_text = Repository.prepare_config(Template().get_repository(),
|
||||||
Repository._config,'repository_name',version_table='_other_table')
|
Repository._config, 'repository_name', version_table='_other_table')
|
||||||
self.assertNotEquals(default_text,specified_text)
|
self.assertNotEquals(default_text, specified_text)
|
||||||
|
@ -1,17 +1,63 @@
|
|||||||
from test import fixture
|
#!/usr/bin/python
|
||||||
from migrate.versioning.repository import *
|
# -*- coding: utf-8 -*-
|
||||||
import os
|
|
||||||
|
|
||||||
class TestPathed(fixture.Base):
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
import migrate.versioning.templates
|
||||||
|
from migrate.versioning.template import *
|
||||||
|
from migrate.versioning import api
|
||||||
|
|
||||||
|
from test import fixture
|
||||||
|
|
||||||
|
|
||||||
|
class TestTemplate(fixture.Pathed):
|
||||||
def test_templates(self):
|
def test_templates(self):
|
||||||
"""We can find the path to all repository templates"""
|
"""We can find the path to all repository templates"""
|
||||||
path = str(template)
|
path = str(Template())
|
||||||
self.assert_(os.path.exists(path))
|
self.assert_(os.path.exists(path))
|
||||||
|
|
||||||
def test_repository(self):
|
def test_repository(self):
|
||||||
"""We can find the path to the default repository"""
|
"""We can find the path to the default repository"""
|
||||||
path = template.get_repository()
|
path = Template().get_repository()
|
||||||
self.assert_(os.path.exists(path))
|
self.assert_(os.path.exists(path))
|
||||||
|
|
||||||
def test_script(self):
|
def test_script(self):
|
||||||
"""We can find the path to the default migration script"""
|
"""We can find the path to the default migration script"""
|
||||||
path = template.get_script()
|
path = Template().get_script()
|
||||||
self.assert_(os.path.exists(path))
|
self.assert_(os.path.exists(path))
|
||||||
|
|
||||||
|
def test_custom_templates_and_themes(self):
|
||||||
|
"""Users can define their own templates with themes"""
|
||||||
|
new_templates_dir = os.path.join(self.temp_usable_dir, 'templates')
|
||||||
|
manage_tmpl_file = os.path.join(new_templates_dir, 'manage/custom.py_tmpl')
|
||||||
|
repository_tmpl_file = os.path.join(new_templates_dir, 'repository/custom/README')
|
||||||
|
script_tmpl_file = os.path.join(new_templates_dir, 'script/custom.py_tmpl')
|
||||||
|
MANAGE_CONTENTS = 'print "manage.py"'
|
||||||
|
README_CONTENTS = 'MIGRATE README!'
|
||||||
|
SCRIPT_FILE_CONTENTS = 'print "script.py"'
|
||||||
|
new_repo_dest = self.tmp_repos()
|
||||||
|
new_manage_dest = self.tmp_py()
|
||||||
|
|
||||||
|
# make new templates dir
|
||||||
|
shutil.copytree(migrate.versioning.templates.__path__[0], new_templates_dir)
|
||||||
|
shutil.copytree(os.path.join(new_templates_dir, 'repository/default'),
|
||||||
|
os.path.join(new_templates_dir, 'repository/custom'))
|
||||||
|
|
||||||
|
# edit templates
|
||||||
|
f = open(manage_tmpl_file, 'w').write(MANAGE_CONTENTS)
|
||||||
|
f = open(repository_tmpl_file, 'w').write(README_CONTENTS)
|
||||||
|
f = open(script_tmpl_file, 'w').write(SCRIPT_FILE_CONTENTS)
|
||||||
|
|
||||||
|
# create repository, manage file and python script
|
||||||
|
kw = {}
|
||||||
|
kw['templates_path'] = new_templates_dir
|
||||||
|
kw['templates_theme'] = 'custom'
|
||||||
|
api.create(new_repo_dest, 'repo_name', **kw)
|
||||||
|
api.script('test', new_repo_dest, **kw)
|
||||||
|
api.manage(new_manage_dest, **kw)
|
||||||
|
|
||||||
|
# assert changes
|
||||||
|
self.assertEqual(open(new_manage_dest).read(), MANAGE_CONTENTS)
|
||||||
|
self.assertEqual(open(os.path.join(new_repo_dest, 'README')).read(), README_CONTENTS)
|
||||||
|
self.assertEqual(open(os.path.join(new_repo_dest, 'versions/001_test.py')).read(), SCRIPT_FILE_CONTENTS)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user