diff --git a/.gitmodules b/.gitmodules index 089002c..f5c5677 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "docs/_build/html"] path = docs/_build/html - url = git@github.com:dinoboff/skeleton.git + url = https://github.com/dinoboff/skeleton.git diff --git a/HISTORY.rst b/HISTORY.rst index 63a5f9e..c3bb32c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,12 @@ History ======= +0.6-ll (Sep 27, 2012) +-------------------- + +Fork for Location Labs. + + 0.6 (Mai 12, 2010) -------------------- diff --git a/setup.py b/setup.py index fd09479..07824d7 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,6 @@ distribute_setup.use_setuptools() import sys - from setuptools import setup @@ -22,7 +21,9 @@ def read_file(name): PROJECT = 'skeleton' -VERSION = '0.6-ypr' +VERSION = '0.6.2-ll' +# Jenkins will replace __build__ with a unique value. +__build__ = '' URL = 'http://dinoboff.github.com/skeleton' AUTHOR = 'Damien Lebrun' AUTHOR_EMAIL = 'dinoboff@gmail.com' @@ -36,7 +37,7 @@ def read_file(name): setup( name=PROJECT, - version=VERSION, + version=VERSION + __build__, description=DESC, long_description=LONG_DESC, author=AUTHOR, @@ -47,18 +48,20 @@ def read_file(name): test_suite='skeleton.tests', include_package_data=True, zip_safe=False, - install_requires=[], + install_requires=[ + 'jinja2' + ], extras_require={ 'virtualenv-templates': [ 'virtualenvwrapper>=2.1.1', 'virtualenvwrapper.project>=1.0' - ], + ], }, entry_points={ 'virtualenvwrapper.project.template': [ 'package = skeleton.examples.basicpackage:virtualenv_warpper_hook', - ], - }, + ], + }, classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Console', @@ -68,6 +71,6 @@ def read_file(name): 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.1', - ], + ], **EXTRAS ) diff --git a/skeleton/__init__.py b/skeleton/__init__.py index 0da33ae..3c00aa8 100644 --- a/skeleton/__init__.py +++ b/skeleton/__init__.py @@ -5,6 +5,7 @@ """ from skeleton.core import ( - Skeleton, Var, Bool, FileNameKeyError, TemplateKeyError + Skeleton, Var, Bool, DependentVar, FileNameKeyError, TemplateKeyError, + Validator, RegexValidator ) from skeleton.utils import insert_into_file diff --git a/skeleton/core.py b/skeleton/core.py index 5c071b8..51dba20 100644 --- a/skeleton/core.py +++ b/skeleton/core.py @@ -11,6 +11,10 @@ import shutil import sys import weakref +import re + +from jinja2 import FileSystemLoader +from jinja2.environment import Environment from skeleton.utils import ( get_loggger, get_file_mode, vars_to_optparser, prompt) @@ -40,7 +44,7 @@ def __init__(self, variable_name, file_path): def __str__(self): return ("Found unexpected variable %r in %r." - % (self.variable_name, self.file_path,)) + % (self.variable_name, self.file_path,)) class FileNameKeyError(KeyError, SkeletonError): @@ -59,8 +63,8 @@ def __init__(self, variable_name, file_path): self.file_path = file_path def __str__(self): - return ("Found unexpected variable %r in file name %r" - % (self.variable_name, self.file_path)) + return ("Found unexpected variable {var} in file name {file}".format(var=self.variable_name, + file=self.file_path)) class ValidateError(SkeletonError): @@ -101,6 +105,43 @@ def wrapper(self, *args, **kw): return wrapper +class StringFormatter(object): + """Format a string template using a skeleton's variables and string.format(). + + """ + + def __init__(self, skeleton): + self.skeleton = skeleton + + def format(self, template): + """Return a formatted version of the string `template`. + + Raises a KeyError if a variable is missing. + """ + return template.format(**self.skeleton) + + +class JinjaFormatter(object): + """Format a string template using a skeleton's variables and Jinja2. + + """ + + def __init__(self, skeleton): + self.skeleton = skeleton + + def format(self, template): + """Return a formatted version of the string `template`. + + Raises a KeyError if a variable is missing. + """ + # preserve final newline, if present + # ref. https://groups.google.com/forum/?fromgroups=#!topic/pocoo-libs/6DylMqq1voI + newline = "\n" if template.endswith("\n") else "" + env = Environment() + env.loader = FileSystemLoader(self.skeleton['__dst_dir__']) + return env.from_string(template + newline).render(**self.skeleton) + + class Skeleton(collections.MutableMapping): """Skeleton Class. @@ -137,6 +178,7 @@ class Skeleton(collections.MutableMapping): template_suffix = '_tmpl' run_dry = False + use_jinja = False def __init__(self, skeleton=None, **kw): self._required_skeletons_instances = None @@ -154,6 +196,11 @@ def __init__(self, skeleton=None, **kw): if var.default is not None: self._defaults[var.name] = var.default + if self.use_jinja: + self.formatter = JinjaFormatter(self) + else: + self.formatter = StringFormatter(self) + @property def required_skeletons_instances(self): """ @@ -172,10 +219,7 @@ def real_src(self): Absolute Path to skeleton directory (read-only). """ if self.src is None: - raise AttributeError( - "The src attribute of the %s Skeleton is not set" % - self.__class__.__name__ - ) + raise AttributeError("The src attribute of the {} Skeleton is not set".format(self.__class__.__name__)) mod = sys.modules[self.__class__.__module__] mod_dir = os.path.dirname(mod.__file__) @@ -238,10 +282,12 @@ def get_missing_variables(self): (even the ones with a default value). """ for var in self.variables: + var.owner = self if var.name not in self.set_variables: self[var.name] = var.do_prompt() else: _LOG.debug("Variable %r already set", var.name) + var.owner = None @run_requirements_first def write(self, dst_dir, run_dry=False): @@ -281,6 +327,8 @@ def write(self, dst_dir, run_dry=False): real_src_len = len(real_src) _LOG.debug("Getting skeleton from %r" % real_src) + self['__dst_dir__'] = dst_dir + for dir_path, dir_names, file_names in os.walk(real_src): rel_dir_path = dir_path[real_src_len:].lstrip(r'\/') rel_dir_path = self._format_file_name(rel_dir_path, real_src) @@ -292,7 +340,7 @@ def write(self, dst_dir, run_dry=False): dst_dir, rel_dir_path, self._format_file_name(file_name, dir_path) - ) + ) self._copy_file(src, dst) #copy directories @@ -332,8 +380,7 @@ def cmd(cls, argv=None, **kw): logging.basicConfig( level=options.verbose_, - format="%(levelname)s - %(message)s" - ) + format="%(levelname)s - %(message)s") for var in skel.variables: value = getattr(options, var.name) @@ -347,11 +394,11 @@ def configure_parser(self): """ parser = optparse.OptionParser(usage="%prog [options] dst_dir") parser.add_option("-q", "--quiet", - action="store_const", const=logging.FATAL, dest="verbose_") + action="store_const", const=logging.FATAL, dest="verbose_") parser.add_option("-v", "--verbose", - action="store_const", const=logging.INFO, dest="verbose_") + action="store_const", const=logging.INFO, dest="verbose_") parser.add_option("-d", "--debug", - action="store_const", const=logging.DEBUG, dest="verbose_") + action="store_const", const=logging.DEBUG, dest="verbose_") parser.set_default('verbose_', logging.ERROR) parser = vars_to_optparser(self.variables, parser=parser) @@ -362,7 +409,7 @@ def template_formatter(self, template): Raises a KeyError if a variable is missing. """ - return template.format(**self) + return self.formatter.format(template) def _format_file_name(self, file_name, dir_path): try: @@ -370,8 +417,7 @@ def _format_file_name(self, file_name, dir_path): except (KeyError,), exc: raise FileNameKeyError( exc.args[0], - os.path.join(dir_path, file_name) - ) + os.path.join(dir_path, file_name)) def _mkdir(self, path, like=None): """Create a directory (using os.mkdir) @@ -418,8 +464,9 @@ def _format_file(self, src, dst): fd_dst = None try: fd_src = codecs.open(src, encoding=self.file_encoding) + rendered_contents = self.template_formatter(fd_src.read()) fd_dst = codecs.open(dst, 'w', encoding=self.file_encoding) - fd_dst.write(self.template_formatter(fd_src.read())) + fd_dst.write(rendered_contents) finally: if fd_src is not None: fd_src.close() @@ -436,6 +483,88 @@ def _set_mode(self, path, like): shutil.copymode(like, path) +class Validator(object): + """Checks that the user has given a non-empty value or that the variable has + a default. + + Returns the valid value or the default. + + Raises a ValidateError if the response is invalid. + """ + + def validate(self, var, response): + """Return a default if appropriate; otherwise check response for validity. + + Subclasses should only need to override do_validate(). + """ + + if not response and var.default is not None: + return var.default + + return self.do_validate(var, response) + + def do_validate(self, var, response): + """Check that input is not empty. + + Raise ValidateError if it is. + """ + if not response: + raise ValidateError("%s is required" % var.display_name) + return response + + +class RegexValidator(Validator): + """Checks either that the user has given a non-empty value matching a regular + expression that the use has given an empty value and the varlible has a default. + + Returns the valid value or the default. + + Raises a ValidateError if the response is invalid. + """ + + def __init__(self, pattern): + """ + Initialize regular expression using a (string) pattern. + + """ + self.pattern = pattern + self.regex = re.compile(pattern) + + def do_validate(self, var, response): + """Check for non-empty, matching input + + Raise ValidateError if the input is non-empty and a non-match. + """ + response = super(RegexValidator, self).do_validate(var, response) + + if not self.regex.match(response): + raise ValidateError("%s does not match required pattern: %r" % (var.display_name, + self.pattern)) + return response + + +class ChoiceValidator(Validator): + """ + Checks that the input matches a choice. + """ + + def __init__(self, choices): + self.choices = choices + + def do_validate(self, var, response): + """ + Check for matching input. + + Raise ValidateError if the input is non-empty and a non-match. + """ + response = super(ChoiceValidator, self).do_validate(var, response) + + if response not in self.choices: + raise ValidateError("{var} must be one of: {choices}".format(var=var.display_name, + choices=", ".join(self.choices))) + return response + + class Var(object): """Define a template variable. @@ -445,21 +574,30 @@ class Var(object): """ _prompt = staticmethod(prompt) - def __init__(self, name, description=None, default=None, intro=None): + def __init__(self, name, description=None, default=None, intro=None, validator=None): self.name = name self.description = description - self.default = default + self._default = default self.intro = intro + self.validator = validator or Validator() + self.owner = None def __repr__(self): return u'<%s %s default=%r>' % ( self.__class__.__name__, self.name, self.default,) + @property + def default(self): + """Return the default value, allowing for customization. + + """ + return self._default + @property def display_name(self): """Return a titled version of name were "_" are replace by spaces. - Allows to get nice looking name at prompt while following pip8 guidance + Allows to get nice looking name at prompt while following pep8 guidance (a Var name can be use as argument of skeleton to set the variable). """ return self.name.replace('_', ' ').title() @@ -487,7 +625,7 @@ def do_prompt(self): """Prompt user for variable value and return the validated value It will keep prompting the user until it receive a valid value. - By default, a value is valid if it is not a empty string string or if + By default, a value is valid if it is not a empty string string or if the variable has a default. If the user value is empty and the variable has a default, the default @@ -504,21 +642,11 @@ def do_prompt(self): except (ValidateError,), exc: print str(exc) - def validate(self, response): - """Checks the user has given a non empty value or that the variable has - a default. - - Returns the valide value or the default. + """Delegate to Validator. - Raises a ValidateError if the response is invalid. """ - if response: - return response - elif self.default is not None: - return self.default - else: - raise ValidateError("%s is required" % self.display_name) + return self.validator.validate(self, response) class Bool(Var): @@ -552,7 +680,7 @@ def validate(self, response): """Checks the response is either Y, YES, N or NO, or that the variable has a default value. - Raises a ValidateError exception if the response wasn't recognized or + Raises a ValidateError exception if the response wasn't recognized or if no value was given and one is required. """ @@ -568,3 +696,21 @@ def validate(self, response): raise ValidateError("%s is required" % self.display_name) else: raise ValidateError('enter either "Y" for yes or "N" or no') + + +class DependentVar(Var): + """Var whose default depends on the value of another Var." + + """ + def __init__(self, name, description=None, intro=None, default=None, validator=None, depends_on=None): + super(DependentVar, self).__init__(name, description, default, intro, validator) + self.depends_on = depends_on + + @property + def default(self): + """If available, use the provided value of another Var as the default. + + """ + if self.owner and self.depends_on: + return str(self.owner[self.depends_on]) + return self._default diff --git a/skeleton/tests/skeletons/jinja/foo.txt_tmpl b/skeleton/tests/skeletons/jinja/foo.txt_tmpl new file mode 100644 index 0000000..db8a8a4 --- /dev/null +++ b/skeleton/tests/skeletons/jinja/foo.txt_tmpl @@ -0,0 +1 @@ +{{ foo }} diff --git a/skeleton/tests/skeletons/jinja/{{bar}}/baz.txt b/skeleton/tests/skeletons/jinja/{{bar}}/baz.txt new file mode 100644 index 0000000..3f95386 --- /dev/null +++ b/skeleton/tests/skeletons/jinja/{{bar}}/baz.txt @@ -0,0 +1 @@ +baz \ No newline at end of file diff --git a/skeleton/tests/test_core.py b/skeleton/tests/test_core.py index 8990327..48a4290 100644 --- a/skeleton/tests/test_core.py +++ b/skeleton/tests/test_core.py @@ -7,7 +7,7 @@ from skeleton.tests.utils import TestCase, TempDir from skeleton.core import Skeleton, Var, TemplateKeyError, FileNameKeyError, \ - Bool + Bool, DependentVar THIS_YEAR = datetime.datetime.utcnow().year @@ -80,6 +80,15 @@ class MissingVariableForFileName(DynamicFileName): """ variables = [] +class Jinja(Static): + """Skeleton dynamic content using jinja syntax""" + + src = 'skeletons/jinja' + use_jinja = True + variables = [ + Var('foo'), + Var('bar'), + ] class TestSkeleton(TestCase): """Tests new implementation of Skeleton""" @@ -332,6 +341,21 @@ def test_overwrite_required_skel(self): with open(tmp_dir.join('foo.txt')) as foo_file: self.assertEqual(foo_file.read().strip(), 'foo') + def test_jinja(self): + """Tests Skeleton.write() with dynamic content using jinja.""" + + skel = Jinja(foo='foo', + bar='bar') + with TempDir() as tmp_dir: + skel.write(tmp_dir.path) + self.assertEqual( + open(tmp_dir.join('foo.txt')).read(), + 'foo\n' + ) + self.assertEqual( + open(tmp_dir.join('bar/baz.txt')).read(), + 'baz' + ) class TestVar(TestCase): """Tests for skeleton.Var""" @@ -484,6 +508,23 @@ def test_template_use_default(self): skel.template_formatter("""{foo} {bar} {baz}"""), """1 2 3""") +class TestDependentVar(TestCase): + """Tests for skeleton.core.DependentVar""" + + def test_use_previous_default(self): + """Tests defaulting to previously specified value""" + + var = DependentVar('foo', depends_on='bar') + var.owner = {'bar':'baz'} + self.assertEqual(var.default, 'baz') + + def test_use_default(self): + """Tests normal defaulting""" + + var = DependentVar('foo', default='bar') + var.owner = {'bar':'baz'} + self.assertEqual(var.default, 'bar') + def suite(): """Get all licence releated test""" @@ -491,8 +532,8 @@ def suite(): tests.addTest(unittest.TestLoader().loadTestsFromTestCase(TestSkeleton)) tests.addTest(unittest.TestLoader().loadTestsFromTestCase(TestVar)) tests.addTest(unittest.TestLoader().loadTestsFromTestCase(TestBool)) - tests.addTest( - unittest.TestLoader().loadTestsFromTestCase(TestDefaultTemplate)) + tests.addTest(unittest.TestLoader().loadTestsFromTestCase(TestDependentVar)) + tests.addTest(unittest.TestLoader().loadTestsFromTestCase(TestDefaultTemplate)) return tests if __name__ == "__main__":