From 5a574f1dce1976eb4c84498a4e7fd677cc5a4cbd Mon Sep 17 00:00:00 2001 From: Jesse Myers Date: Thu, 27 Sep 2012 15:39:36 -0700 Subject: [PATCH 01/23] Change version and update history. --- HISTORY.rst | 6 ++++++ setup.py | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) 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..af6c34e 100755 --- a/setup.py +++ b/setup.py @@ -5,6 +5,7 @@ distribute_setup.use_setuptools() import sys +import os from setuptools import setup @@ -22,7 +23,7 @@ def read_file(name): PROJECT = 'skeleton' -VERSION = '0.6-ypr' +VERSION = '0.6-ll' 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 + os.environ.get('BUILD_SUFFIX',''), description=DESC, long_description=LONG_DESC, author=AUTHOR, From 04645b4f3133036a2925b05fa6c3b12246248991 Mon Sep 17 00:00:00 2001 From: Jesse Myers Date: Sat, 29 Sep 2012 11:23:32 -0700 Subject: [PATCH 02/23] Add DependentVar type that obtains its default value from a previous prompt. This feature allows a skeleton to be constructed with a longer list of variable prompts without necesssarily requiring the user to painfully input every answer. For example, a template to generate a default python project could use the same value of its project name and main package name without typing both -- or could support different values. There are two main changes here: - Var instances get assigned a reference to the calling skeleton ('owner') before prompt() is called. This allows the Var to call back into the skeleton to query what other values have been set without requiring Var instances to know about each other or save state. This reference is cleared after prompt() returns. - Var.default is now a property so its getter can be overridden nicely in the DependentVar subclass. --- skeleton/__init__.py | 2 +- skeleton/core.py | 33 +++++++++++++++++++++++++++++++-- skeleton/tests/test_core.py | 20 +++++++++++++++++++- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/skeleton/__init__.py b/skeleton/__init__.py index 0da33ae..007c95a 100644 --- a/skeleton/__init__.py +++ b/skeleton/__init__.py @@ -5,6 +5,6 @@ """ from skeleton.core import ( - Skeleton, Var, Bool, FileNameKeyError, TemplateKeyError + Skeleton, Var, Bool, DependentVar, FileNameKeyError, TemplateKeyError ) from skeleton.utils import insert_into_file diff --git a/skeleton/core.py b/skeleton/core.py index 5c071b8..c998016 100644 --- a/skeleton/core.py +++ b/skeleton/core.py @@ -238,10 +238,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): @@ -448,18 +450,26 @@ class Var(object): def __init__(self, name, description=None, default=None, intro=None): self.name = name self.description = description - self.default = default + self._default = default self.intro = intro + 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() @@ -568,3 +578,22 @@ 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, depends_on=None): + super(DependentVar,self).__init__(name, description, default, intro) + 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/test_core.py b/skeleton/tests/test_core.py index 8990327..e598a50 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 @@ -484,6 +484,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,6 +508,7 @@ 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(TestDependentVar)) tests.addTest( unittest.TestLoader().loadTestsFromTestCase(TestDefaultTemplate)) return tests From 69d208bfe9301f7f636ac0a620120d3630c6db21 Mon Sep 17 00:00:00 2001 From: Jesse Myers Date: Sat, 29 Sep 2012 11:51:47 -0700 Subject: [PATCH 03/23] Add Validator class and RegexValidator subclass. Validator simply takes over the responsibility formerly in Var.validate(), which now delegates to a validator instance. Var is instantiated with a default Validator if none is provided. RegexValidator takes a pattern as input and validates that the user's input matches the pattern, raising an error if none. Example: variables = [ Var('foo',validator=RegexValidator('[a-z]+')) ] --- skeleton/__init__.py | 3 ++- skeleton/core.py | 63 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/skeleton/__init__.py b/skeleton/__init__.py index 007c95a..3c00aa8 100644 --- a/skeleton/__init__.py +++ b/skeleton/__init__.py @@ -5,6 +5,7 @@ """ from skeleton.core import ( - Skeleton, Var, Bool, DependentVar, 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 c998016..895ab15 100644 --- a/skeleton/core.py +++ b/skeleton/core.py @@ -11,6 +11,7 @@ import shutil import sys import weakref +import re from skeleton.utils import ( get_loggger, get_file_mode, vars_to_optparser, prompt) @@ -437,6 +438,48 @@ def _set_mode(self, path, like): if not self.run_dry: shutil.copymode(like, path) +class Validator(object): + """Define a variable validator. + + """ + + def validate(self, var, response): + """Checks the user has given a non empty value or that the variable has + a default. + + Returns the valide value or the default. + + Raises a ValidateError if the response is invalid. + """ + if self.is_valid(var, response): + return response + elif var.default is not None: + return var.default + else: + raise ValidateError("%s is required" % var.display_name) + + def is_valid(self, var, response): + """Is the user response non-empty? + """ + return response + +class RegexValidator(Validator): + """Validate based on a regular expression. + + """ + + def __init__(self, pattern): + self.pattern = pattern + self.regex = re.compile(pattern) + + def is_valid(self, var, response): + if not response: + return False + + if not self.regex.match(response): + raise ValidateError("%s does not match required pattern: %r" % (var.display_name, + self.pattern)) + return True class Var(object): """Define a template variable. @@ -447,11 +490,12 @@ 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.intro = intro + self.validator = validator or Validator() self.owner = None def __repr__(self): @@ -516,19 +560,10 @@ def do_prompt(self): 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) + self.validator.validate(self, response) class Bool(Var): @@ -584,8 +619,8 @@ class DependentVar(Var): """ - def __init__(self, name, description=None, intro=None, default=None, depends_on=None): - super(DependentVar,self).__init__(name, description, default, intro) + 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 From 3f499655df33c0593fdf2d8292919cc3fa1dac19 Mon Sep 17 00:00:00 2001 From: Jesse Myers Date: Sat, 29 Sep 2012 12:03:41 -0700 Subject: [PATCH 04/23] Refactor validation flow so that extension is clearer. --- skeleton/core.py | 52 +++++++++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/skeleton/core.py b/skeleton/core.py index 895ab15..a819062 100644 --- a/skeleton/core.py +++ b/skeleton/core.py @@ -439,47 +439,63 @@ def _set_mode(self, path, like): shutil.copymode(like, path) class Validator(object): - """Define a variable validator. + """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): - """Checks the user has given a non empty value or that the variable has - a default. - - Returns the valide value or the default. + """Return a default if appropriate; otherwise check response for validity. - Raises a ValidateError if the response is invalid. + Subclasses should only need to override do_validate(). """ - if self.is_valid(var, response): - return response - elif var.default is not None: + + if not response and var.default is not None: return var.default - else: - raise ValidateError("%s is required" % var.display_name) - def is_valid(self, var, response): - """Is the user response non-empty? + 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): - """Validate based on a regular expression. + """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 is_valid(self, var, response): - if not response: - return False + 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 True + return response + class Var(object): """Define a template variable. From d7ef380e03db9f9ef730bb8876c1e16bfa028d00 Mon Sep 17 00:00:00 2001 From: Jesse Myers Date: Mon, 1 Oct 2012 13:29:53 -0700 Subject: [PATCH 05/23] Fix bug: return validated value. --- skeleton/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skeleton/core.py b/skeleton/core.py index a819062..8c1b695 100644 --- a/skeleton/core.py +++ b/skeleton/core.py @@ -579,7 +579,7 @@ def validate(self, response): """Delegate to Validator. """ - self.validator.validate(self, response) + return self.validator.validate(self, response) class Bool(Var): From 5ad4c5068bdea5ed0075dc728548922363bfe35b Mon Sep 17 00:00:00 2001 From: Jesse Myers Date: Mon, 1 Oct 2012 14:19:58 -0700 Subject: [PATCH 06/23] Add support for jinja-based formatting: - Formatting now uses a formatter class - StringFormatter remains the default - JinjaFormatter may be used trivially by setting 'use_jinja' --- setup.py | 4 +- skeleton/core.py | 40 ++++++++++++++++++- skeleton/tests/skeletons/jinja/foo.txt_tmpl | 1 + .../tests/skeletons/jinja/{{bar}}/baz.txt | 1 + skeleton/tests/test_core.py | 27 ++++++++++++- 5 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 skeleton/tests/skeletons/jinja/foo.txt_tmpl create mode 100644 skeleton/tests/skeletons/jinja/{{bar}}/baz.txt diff --git a/setup.py b/setup.py index af6c34e..9a3c809 100755 --- a/setup.py +++ b/setup.py @@ -48,7 +48,9 @@ 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', diff --git a/skeleton/core.py b/skeleton/core.py index 8c1b695..b37e857 100644 --- a/skeleton/core.py +++ b/skeleton/core.py @@ -13,6 +13,8 @@ import weakref import re +from jinja2 import Template + from skeleton.utils import ( get_loggger, get_file_mode, vars_to_optparser, prompt) @@ -101,6 +103,36 @@ def wrapper(self, *args, **kw): functools.update_wrapper(wrapper, skel_method) 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. + """ + return Template(template).render(**self.skeleton) + class Skeleton(collections.MutableMapping): """Skeleton Class. @@ -138,6 +170,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 @@ -155,6 +188,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): """ @@ -365,7 +403,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: 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 e598a50..09bfc41 100644 --- a/skeleton/tests/test_core.py +++ b/skeleton/tests/test_core.py @@ -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().strip(), + 'foo' + ) + self.assertEqual( + open(tmp_dir.join('bar/baz.txt')).read().strip(), + 'baz' + ) class TestVar(TestCase): """Tests for skeleton.Var""" @@ -509,8 +533,7 @@ def suite(): tests.addTest(unittest.TestLoader().loadTestsFromTestCase(TestVar)) tests.addTest(unittest.TestLoader().loadTestsFromTestCase(TestBool)) tests.addTest(unittest.TestLoader().loadTestsFromTestCase(TestDependentVar)) - tests.addTest( - unittest.TestLoader().loadTestsFromTestCase(TestDefaultTemplate)) + tests.addTest(unittest.TestLoader().loadTestsFromTestCase(TestDefaultTemplate)) return tests if __name__ == "__main__": From 488a6e1b26b57074b4b2d5217920dfd33b8e9bd8 Mon Sep 17 00:00:00 2001 From: Dave Charness Date: Wed, 10 Oct 2012 12:51:25 -0700 Subject: [PATCH 07/23] Preserve final newline in Jinja templates As described in the pocoo-libs thread "Bug in jinja2.lexer", https://groups.google.com/forum/?fromgroups=#!topic/pocoo-libs/6DylMqq1voI Jinja normalizes newlines in a manner that loses a newline at the end of a file. Add a check for the trailing newline in a template to be rendered by JinjaFormatter and add an extra newline to preserve the original in the output. --- skeleton/core.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/skeleton/core.py b/skeleton/core.py index b37e857..47cd123 100644 --- a/skeleton/core.py +++ b/skeleton/core.py @@ -131,7 +131,10 @@ def format(self, template): Raises a KeyError if a variable is missing. """ - return Template(template).render(**self.skeleton) + # preserve final newline, if present + # ref. https://groups.google.com/forum/?fromgroups=#!topic/pocoo-libs/6DylMqq1voI + newline = "\n" if template.endswith("\n") else "" + return Template(template + newline).render(**self.skeleton) class Skeleton(collections.MutableMapping): From 502b8396522c5719fe3ee9efbbbd749bee460b98 Mon Sep 17 00:00:00 2001 From: Dave Charness Date: Wed, 10 Oct 2012 13:06:13 -0700 Subject: [PATCH 08/23] test_core: check preservation of trailing newlines Forgot to update tests to reflect my preceding change --- skeleton/tests/test_core.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skeleton/tests/test_core.py b/skeleton/tests/test_core.py index 09bfc41..48a4290 100644 --- a/skeleton/tests/test_core.py +++ b/skeleton/tests/test_core.py @@ -349,11 +349,11 @@ def test_jinja(self): with TempDir() as tmp_dir: skel.write(tmp_dir.path) self.assertEqual( - open(tmp_dir.join('foo.txt')).read().strip(), - 'foo' + open(tmp_dir.join('foo.txt')).read(), + 'foo\n' ) self.assertEqual( - open(tmp_dir.join('bar/baz.txt')).read().strip(), + open(tmp_dir.join('bar/baz.txt')).read(), 'baz' ) From 0b07cb108e52017f32f071c692279834d4066f3a Mon Sep 17 00:00:00 2001 From: Jesse Myers Date: Fri, 12 Oct 2012 11:46:58 -0700 Subject: [PATCH 09/23] Support BUILD_SUFFIX to add ".dev" extension to version for automated builds. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9a3c809..cba54c5 100755 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ def read_file(name): PROJECT = 'skeleton' -VERSION = '0.6-ll' +VERSION = '0.6-ll' + os.environ.get('BUILD_SUFFIX','') URL = 'http://dinoboff.github.com/skeleton' AUTHOR = 'Damien Lebrun' AUTHOR_EMAIL = 'dinoboff@gmail.com' From 762e8f716127789fc840a14305f08747f0f040b1 Mon Sep 17 00:00:00 2001 From: Jesse Myers Date: Fri, 12 Oct 2012 11:49:37 -0700 Subject: [PATCH 10/23] Remove duplicate suffix. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index cba54c5..9a3c809 100755 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ def read_file(name): PROJECT = 'skeleton' -VERSION = '0.6-ll' + os.environ.get('BUILD_SUFFIX','') +VERSION = '0.6-ll' URL = 'http://dinoboff.github.com/skeleton' AUTHOR = 'Damien Lebrun' AUTHOR_EMAIL = 'dinoboff@gmail.com' From 596f2b93ebf2ebfee3a7688ef24766364f15e22f Mon Sep 17 00:00:00 2001 From: Jesse Myers Date: Fri, 12 Oct 2012 12:04:37 -0700 Subject: [PATCH 11/23] Change submodule to read only https form. --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From e64fc5d9af7ca6b14a5cdb33d8f7665d62275703 Mon Sep 17 00:00:00 2001 From: Jesse Myers Date: Sat, 22 Dec 2012 06:20:17 -0800 Subject: [PATCH 12/23] Use __build__ instead of os.environ --- setup.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index 9a3c809..b507a0e 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,6 @@ distribute_setup.use_setuptools() import sys -import os from setuptools import setup @@ -24,6 +23,8 @@ def read_file(name): PROJECT = 'skeleton' VERSION = '0.6-ll' +# Jenkins will replace __build__ with a unique value. +__build__ = '' URL = 'http://dinoboff.github.com/skeleton' AUTHOR = 'Damien Lebrun' AUTHOR_EMAIL = 'dinoboff@gmail.com' @@ -37,7 +38,7 @@ def read_file(name): setup( name=PROJECT, - version=VERSION + os.environ.get('BUILD_SUFFIX',''), + version=VERSION + __build__, description=DESC, long_description=LONG_DESC, author=AUTHOR, @@ -50,18 +51,18 @@ def read_file(name): zip_safe=False, 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', @@ -71,6 +72,6 @@ def read_file(name): 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.1', - ], + ], **EXTRAS ) From d39c7a0a1026ae9e5987c763332c6d16035b5f25 Mon Sep 17 00:00:00 2001 From: Jesse Myers Date: Sat, 22 Dec 2012 06:24:37 -0800 Subject: [PATCH 13/23] Testing push for RC --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index b507a0e..94c3bc3 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,6 @@ distribute_setup.use_setuptools() import sys - from setuptools import setup From 86afc4a9ba811533d7c072d1145610a17a041adc Mon Sep 17 00:00:00 2001 From: Jesse Myers Date: Sat, 22 Dec 2012 06:25:16 -0800 Subject: [PATCH 14/23] Rev version of master. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b507a0e..5b166cd 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def read_file(name): PROJECT = 'skeleton' -VERSION = '0.6-ll' +VERSION = '0.6.1-ll' # Jenkins will replace __build__ with a unique value. __build__ = '' URL = 'http://dinoboff.github.com/skeleton' From 8f1b306bd2a17c5c57942edb7ed6be5f0a014eb9 Mon Sep 17 00:00:00 2001 From: Jesse Myers Date: Thu, 7 Mar 2013 22:04:06 -0800 Subject: [PATCH 15/23] PEP8 compliance --- skeleton/core.py | 45 +++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/skeleton/core.py b/skeleton/core.py index 47cd123..e46f2d1 100644 --- a/skeleton/core.py +++ b/skeleton/core.py @@ -62,8 +62,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): @@ -103,6 +103,7 @@ def wrapper(self, *args, **kw): functools.update_wrapper(wrapper, skel_method) return wrapper + class StringFormatter(object): """Format a string template using a skeleton's variables and string.format(). @@ -118,6 +119,7 @@ def format(self, template): """ return template.format(**self.skeleton) + class JinjaFormatter(object): """Format a string template using a skeleton's variables and Jinja2. @@ -214,10 +216,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__) @@ -376,8 +375,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) @@ -391,11 +389,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) @@ -414,8 +412,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) @@ -479,12 +476,13 @@ def _set_mode(self, path, like): if not self.run_dry: 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. """ @@ -508,10 +506,11 @@ def do_validate(self, var, 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 + """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. @@ -530,7 +529,7 @@ def do_validate(self, var, response): Raise ValidateError if the input is non-empty and a non-match. """ - response = super(RegexValidator,self).do_validate(var, response) + 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, @@ -565,7 +564,7 @@ def default(self): """ return self._default - + @property def display_name(self): """Return a titled version of name were "_" are replace by spaces. @@ -598,7 +597,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 @@ -615,7 +614,6 @@ def do_prompt(self): except (ValidateError,), exc: print str(exc) - def validate(self, response): """Delegate to Validator. @@ -654,7 +652,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. """ @@ -671,13 +669,13 @@ def validate(self, response): 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) + super(DependentVar, self).__init__(name, description, default, intro, validator) self.depends_on = depends_on @property @@ -688,4 +686,3 @@ def default(self): if self.owner and self.depends_on: return str(self.owner[self.depends_on]) return self._default - From 5eed3e619e3571452384c72520f029e618fef09e Mon Sep 17 00:00:00 2001 From: Jesse Myers Date: Fri, 22 Mar 2013 16:06:41 -0700 Subject: [PATCH 16/23] Add choice validator. --- skeleton/core.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/skeleton/core.py b/skeleton/core.py index e46f2d1..e3dd060 100644 --- a/skeleton/core.py +++ b/skeleton/core.py @@ -43,7 +43,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): @@ -335,7 +335,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 @@ -537,6 +537,28 @@ def do_validate(self, var, response): 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. From 1c257ef8a2245f1d7d3f0628dd29f381a97e1d46 Mon Sep 17 00:00:00 2001 From: Jesse Myers Date: Mon, 25 Mar 2013 10:48:29 -0700 Subject: [PATCH 17/23] Bump version. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 38514b4..07824d7 100755 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def read_file(name): PROJECT = 'skeleton' -VERSION = '0.6.1-ll' +VERSION = '0.6.2-ll' # Jenkins will replace __build__ with a unique value. __build__ = '' URL = 'http://dinoboff.github.com/skeleton' From 6f3f15fc878d1229485d5950f98114e080189f45 Mon Sep 17 00:00:00 2001 From: Henry Finucane Date: Sun, 26 Jan 2014 21:32:55 -0800 Subject: [PATCH 18/23] Add the ability to include other files --- skeleton/core.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/skeleton/core.py b/skeleton/core.py index e3dd060..6daf3e6 100644 --- a/skeleton/core.py +++ b/skeleton/core.py @@ -13,7 +13,8 @@ import weakref import re -from jinja2 import Template +from jinja2 import FileSystemLoader +from jinja2.environment import Environment from skeleton.utils import ( get_loggger, get_file_mode, vars_to_optparser, prompt) @@ -136,7 +137,9 @@ def format(self, template): # preserve final newline, if present # ref. https://groups.google.com/forum/?fromgroups=#!topic/pocoo-libs/6DylMqq1voI newline = "\n" if template.endswith("\n") else "" - return Template(template + newline).render(**self.skeleton) + env = Environment() + env.loader = FileSystemLoader('.') + return env.from_string(template + newline).render(**self.skeleton) class Skeleton(collections.MutableMapping): From 19e6e35c9747707da34d7e6697d99b74106698c3 Mon Sep 17 00:00:00 2001 From: Henry Finucane Date: Sun, 26 Jan 2014 22:45:37 -0800 Subject: [PATCH 19/23] Allow including rendered files By delaying the file open, you can read the existing file --- skeleton/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skeleton/core.py b/skeleton/core.py index 6daf3e6..f4eb331 100644 --- a/skeleton/core.py +++ b/skeleton/core.py @@ -462,8 +462,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() From 19b1945ad02d6a74018ff51ea953adb37ac38786 Mon Sep 17 00:00:00 2001 From: Henry Finucane Date: Fri, 31 Jan 2014 18:17:25 -0800 Subject: [PATCH 20/23] Make destination directory available to templates --- skeleton/core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skeleton/core.py b/skeleton/core.py index f4eb331..13a5fbe 100644 --- a/skeleton/core.py +++ b/skeleton/core.py @@ -385,6 +385,8 @@ def cmd(cls, argv=None, **kw): if value is not None: skel[var.name] = value + skel['__dst_dir__'] = args[0] + skel.run(args[0]) def configure_parser(self): From c328815f741b638df8a4cfbed90822ab97b8e022 Mon Sep 17 00:00:00 2001 From: Henry Finucane Date: Mon, 3 Feb 2014 11:55:47 -0800 Subject: [PATCH 21/23] Allow loading from non-cwd destination directory --- setup.py | 2 +- skeleton/core.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 07824d7..ba56856 100755 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def read_file(name): PROJECT = 'skeleton' -VERSION = '0.6.2-ll' +VERSION = '0.6.3-ll' # Jenkins will replace __build__ with a unique value. __build__ = '' URL = 'http://dinoboff.github.com/skeleton' diff --git a/skeleton/core.py b/skeleton/core.py index 13a5fbe..2ce63dd 100644 --- a/skeleton/core.py +++ b/skeleton/core.py @@ -138,7 +138,7 @@ def format(self, template): # ref. https://groups.google.com/forum/?fromgroups=#!topic/pocoo-libs/6DylMqq1voI newline = "\n" if template.endswith("\n") else "" env = Environment() - env.loader = FileSystemLoader('.') + env.loader = FileSystemLoader(self.skeleton['__dst_dir__']) return env.from_string(template + newline).render(**self.skeleton) From c533b898d4895dfc7d36b00399596fb4f961bfbe Mon Sep 17 00:00:00 2001 From: Henry Finucane Date: Mon, 3 Feb 2014 12:44:34 -0800 Subject: [PATCH 22/23] Set __dst_dir__ in write() instead of cmd() Tests pass now --- skeleton/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skeleton/core.py b/skeleton/core.py index 2ce63dd..51dba20 100644 --- a/skeleton/core.py +++ b/skeleton/core.py @@ -327,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) @@ -385,8 +387,6 @@ def cmd(cls, argv=None, **kw): if value is not None: skel[var.name] = value - skel['__dst_dir__'] = args[0] - skel.run(args[0]) def configure_parser(self): From 174dcd8943b34acc97840fb26ecbfafb2cb3e905 Mon Sep 17 00:00:00 2001 From: Jesse Myers Date: Mon, 3 Feb 2014 13:48:29 -0800 Subject: [PATCH 23/23] Restore version to 0.6.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ba56856..07824d7 100755 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def read_file(name): PROJECT = 'skeleton' -VERSION = '0.6.3-ll' +VERSION = '0.6.2-ll' # Jenkins will replace __build__ with a unique value. __build__ = '' URL = 'http://dinoboff.github.com/skeleton'