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 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.

View File

@ -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

View File

@ -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):

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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])

View File

@ -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)

View File

@ -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)