add option to customize templates and use multiple themes

This commit is contained in:
iElectric 2009-07-28 15:52:59 +02:00
parent 7cb4b6363c
commit 78ce747e25
9 changed files with 173 additions and 97 deletions

View File

@ -1,6 +1,7 @@
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)
- 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.

View File

@ -493,3 +493,25 @@ currently:
the databases your application will actually be using to ensure your
updates to that database work properly. This must be a list;
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

View File

@ -264,7 +264,7 @@ def manage(file, **opts):
python manage.py version
%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):

View File

@ -4,10 +4,10 @@
import os
import shutil
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.template import template
from migrate.versioning.template import Template
from migrate.versioning.base import *
@ -91,11 +91,18 @@ class Repository(pathed.Pathed):
except exceptions.PathNotFoundError, e:
raise exceptions.InvalidRepositoryError(path)
# TODO: what are those options?
@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.
: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
defaults = dict(
@ -105,7 +112,7 @@ class Repository(pathed.Pathed):
defaults.update(opts)
tmpl = resource_string(pkg, rsrc)
tmpl = open(os.path.join(tmpl_dir, config_file)).read()
ret = string.Template(tmpl).substitute(defaults)
return ret
@ -113,14 +120,12 @@ class Repository(pathed.Pathed):
def create(cls, path, name, **opts):
"""Create a repository at a specified path"""
cls.require_notfound(path)
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)
theme = opts.get('templates_theme', None)
# 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
fd = open(os.path.join(path, cls._config), 'w')
@ -129,7 +134,7 @@ class Repository(pathed.Pathed):
# Create a management script
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)
@ -205,12 +210,10 @@ class Repository(pathed.Pathed):
:param file_: Destination file to be written
: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()])
pkg, rsrc = template.manage(as_pkg=True)
tmpl = resource_string(pkg, rsrc)
result = tmpl % dict(defaults=vars_)
tmpl = open(mng_file).read()
fd = open(file_, 'w')
fd.write(result)
fd.write(tmpl % dict(defaults=vars_))
fd.close()

View File

@ -7,7 +7,7 @@ from StringIO import StringIO
import migrate
from migrate.versioning import exceptions, genmodel, schemadiff
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.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>`"""
cls.require_notfound(path)
# TODO: Use the default script template (defined in the template
# 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)
src = Template(opts.pop('templates_path', None)).get_script(theme=opts.pop('templates_theme', None))
shutil.copy(src, path)
return cls(path)
@ -67,8 +63,7 @@ class PythonScript(base.BaseScript):
genmodel.ModelGenerator(diff).toUpgradeDowngradePython()
# Store differences into file.
# TODO: add custom templates
src = template.get_script(None)
src = Template(opts.pop('templates_path', None)).get_script(opts.pop('templates_theme', None))
f = open(src)
contents = f.read()
f.close()

View File

@ -4,81 +4,84 @@
import os
import shutil
import sys
from pkg_resources import resource_filename
from migrate.versioning.base import *
from migrate.versioning import pathed
class Packaged(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):
class Collection(pathed.Pathed):
"""A collection of templates of a specific type"""
_default = None
_mask = None
def get_path(self, file):
return os.path.join(self.path, str(file))
def get_pkg(self, file):
return (self.pkg, str(file))
class RepositoryCollection(Collection):
_default = 'default'
_mask = '%s'
class ScriptCollection(Collection):
_default = 'default.py_tmpl'
_mask = '%s.py_tmpl'
class ManageCollection(Collection):
_mask = '%s.py_tmpl'
class Template(Packaged):
"""Finds the paths/packages of various Migrate templates"""
_repository = 'repository'
_script = 'script'
class Template(pathed.Pathed):
"""Finds the paths/packages of various Migrate templates.
:param path: Templates are loaded from migrate package
if `path` is not provided.
"""
pkg = 'migrate.versioning.templates'
_manage = 'manage.py_tmpl'
def __init__(self, pkg):
super(Template, self).__init__(pkg)
self.repository = RepositoryCollection('.'.join((self.pkg,
self._repository)))
self.script = ScriptCollection('.'.join((self.pkg, self._script)))
def __new__(cls, path=None):
if path is None:
path = cls._find_path(cls.pkg)
return super(Template, cls).__new__(cls, path)
def get_item(self, attr, filename=None, as_pkg=None, as_str=None):
item = getattr(self, attr)
if filename is None:
filename = getattr(item, '_default')
if as_pkg:
ret = item.get_pkg(filename)
if as_str:
ret = '.'.join(ret)
def __init__(self, path=None):
if path is None:
path = Template._find_path(self.pkg)
super(Template, self).__init__(path)
self.repository = RepositoryCollection(os.path.join(path, 'repository'))
self.script = ScriptCollection(os.path.join(path, 'script'))
self.manage = ManageCollection(os.path.join(path, 'manage'))
@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:
ret = item.get_path(filename)
return ret
return resource_filename(tmp_pkg[0], '')
def get_repository(self, filename=None, as_pkg=None, as_str=None):
return self.get_item('repository', filename, as_pkg, as_str)
def _get_item(self, collection, theme=None):
"""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):
return self.get_item('script', filename, as_pkg, as_str)
def get_script(self, *a, **kw):
"""Calls self._get_item('script', *a, **kw)"""
return self._get_item('script', *a, **kw)
def manage(self, **k):
return (self.pkg, self._manage)
template_pkg = 'migrate.versioning.templates'
template = Template(template_pkg)
def get_manage(self, *a, **kw):
"""Calls self._get_item('manage', *a, **kw)"""
return self._get_item('manage', *a, **kw)

View File

@ -101,7 +101,7 @@ class Collection(pathed.Pathed):
if os.path.exists(filepath):
raise Exception('Script already exists: %s' % filepath)
else:
script.PythonScript.create(filepath)
script.PythonScript.create(filepath, **k)
self.versions[ver] = Version(ver, self.path, [filename])

View File

@ -1,21 +1,27 @@
from test import fixture
#!/usr/bin/python
# -*- coding: utf-8 -*-
from migrate.versioning import cfgparse
from migrate.versioning.repository import *
from migrate.versioning.template import Template
from test import fixture
class TestConfigParser(fixture.Base):
def test_to_dict(self):
"""Correctly interpret config results as dictionaries"""
parser = cfgparse.Parser(dict(default_value=42))
self.assert_(len(parser.sections())==0)
self.assert_(len(parser.sections()) == 0)
parser.add_section('section')
parser.set('section','option','value')
self.assert_(parser.get('section','option')=='value')
self.assert_(parser.to_dict()['section']['option']=='value')
self.assertEqual(parser.get('section', 'option'), 'value')
self.assertEqual(parser.to_dict()['section']['option'], 'value')
def test_table_config(self):
"""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),
Repository._config,'repository_name')
specified_text=Repository.prepare_config(template.get_repository(as_pkg=True,as_str=True),
Repository._config,'repository_name',version_table='_other_table')
self.assertNotEquals(default_text,specified_text)
default_text = Repository.prepare_config(Template().get_repository(),
Repository._config, 'repository_name')
specified_text = Repository.prepare_config(Template().get_repository(),
Repository._config, 'repository_name', version_table='_other_table')
self.assertNotEquals(default_text, specified_text)

View File

@ -1,17 +1,63 @@
from test import fixture
from migrate.versioning.repository import *
import os
#!/usr/bin/python
# -*- coding: utf-8 -*-
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):
"""We can find the path to all repository templates"""
path = str(template)
path = str(Template())
self.assert_(os.path.exists(path))
def test_repository(self):
"""We can find the path to the default repository"""
path = template.get_repository()
path = Template().get_repository()
self.assert_(os.path.exists(path))
def test_script(self):
"""We can find the path to the default migration script"""
path = template.get_script()
path = Template().get_script()
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)