[Checkins] SVN: z3c.recipe.filetemplate/trunk/ merge work on lp:~gary/z3c.recipe.filetemplate/relative-paths

Gary Poster gary.poster at canonical.com
Wed Apr 21 13:55:06 EDT 2010


Log message for revision 111210:
  merge work on lp:~gary/z3c.recipe.filetemplate/relative-paths

Changed:
  A   z3c.recipe.filetemplate/trunk/.bzrignore
  U   z3c.recipe.filetemplate/trunk/CHANGES.txt
  A   z3c.recipe.filetemplate/trunk/MANIFEST.in
  U   z3c.recipe.filetemplate/trunk/buildout.cfg
  U   z3c.recipe.filetemplate/trunk/setup.py
  U   z3c.recipe.filetemplate/trunk/z3c/recipe/filetemplate/README.txt
  U   z3c.recipe.filetemplate/trunk/z3c/recipe/filetemplate/__init__.py
  U   z3c.recipe.filetemplate/trunk/z3c/recipe/filetemplate/tests.py
  U   z3c.recipe.filetemplate/trunk/z3c/recipe/filetemplate/tests.txt

-=-
Added: z3c.recipe.filetemplate/trunk/.bzrignore
===================================================================
--- z3c.recipe.filetemplate/trunk/.bzrignore	                        (rev 0)
+++ z3c.recipe.filetemplate/trunk/.bzrignore	2010-04-21 17:55:06 UTC (rev 111210)
@@ -0,0 +1,7 @@
+.installed.cfg
+bin
+develop-eggs
+eggs
+parts
+z3c.recipe.filetemplate.egg-info
+dist

Modified: z3c.recipe.filetemplate/trunk/CHANGES.txt
===================================================================
--- z3c.recipe.filetemplate/trunk/CHANGES.txt	2010-04-21 17:13:11 UTC (rev 111209)
+++ z3c.recipe.filetemplate/trunk/CHANGES.txt	2010-04-21 17:55:06 UTC (rev 111210)
@@ -9,9 +9,51 @@
 Features
 --------
 
-- None yet.
+- Enable cross-platform paths by allowing an extended syntax for path
+  suffixes. Example: If ``${buildout:directory}`` resolves to
+  ``/sample_buildout`` on a POSIX system and ``C:\sample_buildout`` in
+  Windows, ``${buildout:directory/foo.txt}`` will resolve to
+  ``/sample_buildout/foo.txt`` and ``C:\sample_buildout\foo.txt``,
+  respectively.
 
+- Add filters via a pipe syntax, reminiscent of UNIX pipes or Django template
+  filters. Simple example: if ``${name}`` resolves to ``harry`` then
+  ``${name|upper}`` resolves to ``HARRY``.  Simple string filters are
+  upper, lower, title, and capitalize, just like the Python string
+  methods.  Also see the next bullet.
 
+- Added support for the buildout relative-paths option.  Shell scripts should
+  include ``${shell-relative-path-setup}`` before commands with
+  buildout-generated paths are executed.  Python scripts should use
+  ``${python-relative-path-setup}`` similarly.  ``${os-paths}`` (shell),
+  ``${space-paths}`` (shell), and ``${string-paths}`` (Python) will have
+  relative paths if the buildout relative-paths option is used.  To convert
+  individual absolute paths to relative paths, use the ``path-repr`` filter
+  in Python scripts and the ``shell-path`` filter in shell scripts.  Path
+  suffixes can be combined with these filters, so, if buildout's
+  relative-paths option is true, ``${buildout:directory/foo.txt|path-repr}``
+  will produce a buildout-relative, platform appropriate path to
+  foo.txt.  Note that for shell scripts, Windows is not supported at
+  this time.
+
+- Support escaping ``${...}`` with ``$${...}`` in templates.  This is
+  particularly useful for *NIX shell scripts.
+
+-----
+Fixes
+-----
+
+- Make tests less susceptible to timing errors.
+
+-------
+Changes
+-------
+
+- ``${os-paths}`` and ``${space-paths}`` no longer filter out .zip paths.
+
+- The entries in ``${string-paths}`` now are separated by newlines. Each
+  entry is indented to the level of the initial placement of the marker.
+
 2.0.3 (2009-07-02)
 ==================
 

Added: z3c.recipe.filetemplate/trunk/MANIFEST.in
===================================================================
--- z3c.recipe.filetemplate/trunk/MANIFEST.in	                        (rev 0)
+++ z3c.recipe.filetemplate/trunk/MANIFEST.in	2010-04-21 17:55:06 UTC (rev 111210)
@@ -0,0 +1,3 @@
+include *.txt
+recursive-include z3c *.txt
+exclude MANIFEST.in buildout.cfg .bzrignore

Modified: z3c.recipe.filetemplate/trunk/buildout.cfg
===================================================================
--- z3c.recipe.filetemplate/trunk/buildout.cfg	2010-04-21 17:13:11 UTC (rev 111209)
+++ z3c.recipe.filetemplate/trunk/buildout.cfg	2010-04-21 17:55:06 UTC (rev 111210)
@@ -1,7 +1,14 @@
 [buildout]
 develop = .
 parts = test
+        interpreter
 
 [test]
 recipe = zc.recipe.testrunner
 eggs = z3c.recipe.filetemplate
+
+[interpreter]
+recipe = zc.recipe.egg
+interpreter = py
+eggs = z3c.recipe.filetemplate
+

Modified: z3c.recipe.filetemplate/trunk/setup.py
===================================================================
--- z3c.recipe.filetemplate/trunk/setup.py	2010-04-21 17:13:11 UTC (rev 111209)
+++ z3c.recipe.filetemplate/trunk/setup.py	2010-04-21 17:55:06 UTC (rev 111210)
@@ -19,7 +19,7 @@
     return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
 
 setup(name='z3c.recipe.filetemplate',
-      version = '2.1dev',
+      version = '2.1',
       license='ZPL 2.1',
       url='http://pypi.python.org/pypi/z3c.recipe.filetemplate',
       description="zc.buildout recipe for creating files from file templates",

Modified: z3c.recipe.filetemplate/trunk/z3c/recipe/filetemplate/README.txt
===================================================================
--- z3c.recipe.filetemplate/trunk/z3c/recipe/filetemplate/README.txt	2010-04-21 17:13:11 UTC (rev 111209)
+++ z3c.recipe.filetemplate/trunk/z3c/recipe/filetemplate/README.txt	2010-04-21 17:55:06 UTC (rev 111210)
@@ -40,7 +40,7 @@
     ... world = Philipp
     ... """)
 
-After executing buildout, we can see that ``$world`` has indeed been
+After executing buildout, we can see that ``${world}`` has indeed been
 replaced by ``Philipp``:
 
     >>> print system(buildout)
@@ -49,6 +49,35 @@
     >>> cat(sample_buildout, 'helloworld.txt')
     Hello Philipp!
 
+If you need to escape the ${...} pattern, you can do so by repeating the dollar
+sign.
+
+    >>> update_file(sample_buildout, 'helloworld.txt.in',
+    ... """
+    ... Hello world! The double $${dollar-sign} escapes!
+    ... """)
+
+    >>> print system(buildout)
+    Uninstalling message.
+    Installing message.
+
+    >>> cat(sample_buildout, 'helloworld.txt')
+    Hello world! The double ${dollar-sign} escapes!
+
+Note that dollar signs alone, without curly braces, are not parsed.
+
+    >>> update_file(sample_buildout, 'helloworld.txt.in',
+    ... """
+    ... $Hello $$world! $$$profit!
+    ... """)
+
+    >>> print system(buildout)
+    Uninstalling message.
+    Installing message.
+
+    >>> cat(sample_buildout, 'helloworld.txt')
+    $Hello $$world! $$$profit!
+
 Note that the output file uses the same permission bits as found on the input
 file.
 
@@ -195,6 +224,12 @@
 Also note that, if you use a source directory and your ``files`` specify a
 directory, the directory must match precisely.
 
+    >>> # Clean up for later test.
+    >>> import shutil
+    >>> shutil.rmtree(os.path.join(sample_buildout, 'template', 'etc'))
+    >>> os.remove(os.path.join(
+    ...     sample_buildout, 'template', 'bin', 'helloworld.sh.in'))
+
 ==============
 Advanced Usage
 ==============
@@ -206,7 +241,7 @@
 standard buildout syntax, but used in the template.  Notice
 ``${buildout:parts}`` in the template below.
 
-    >>> write(sample_buildout, 'helloworld.txt.in',
+    >>> update_file(sample_buildout, 'helloworld.txt.in',
     ... """
     ... Hello ${world}.  I used these parts: ${buildout:parts}.
     ... """)
@@ -228,21 +263,118 @@
     >>> cat(sample_buildout, 'helloworld.txt')
     Hello Philipp.  I used these parts: message.
 
-Sharing variables
+Path Extensions
+===============
+
+Substitutions can have path suffixes using the POSIX "/" path separator.
+The template will convert these to the proper path separator for the current
+OS.  They also then are part of the value passed to filters, the feature
+described next.  Notice ``${buildout:directory/foo/bar.txt}`` in the template
+below.
+
+    >>> update_file(sample_buildout, 'helloworld.txt.in',
+    ... """
+    ... Here's foo/bar.txt in the buildout:
+    ... ${buildout:directory/foo/bar.txt}
+    ... """)
+
+    >>> print system(buildout)
+    Uninstalling message.
+    Installing message.
+
+    >>> cat(sample_buildout, 'helloworld.txt') # doctest: +ELLIPSIS
+    Here's foo/bar.txt in the buildout:
+    /.../sample-buildout/foo/bar.txt
+
+Filters
+=======
+
+You can use pipes within a substitution to filter the original value.  This
+recipe provides several filters for you to use.  The syntax is reminiscent of
+(and inspired by) POSIX pipes and Django template filters.  For example,
+if world = Philipp, ``HELLO ${world|upper}!`` would result in ``HELLO
+PHILIPP!``.
+
+A few simple Python string methods are exposed as filters right now:
+
+- capitalize: First letter in string is capitalized.
+- lower: All letters in string are lowercase.
+- title: First letter of each word in string is capitalized.
+- upper: All letters in string are uppercase.
+
+Other filters are important for handling paths if buildout's relative-paths
+option is true.  See `Working with Paths`_ for more details.
+
+- path-repr: Converts the path to a Python expression for the path.  If
+  buildout's relative-paths option is false, this will simply be a repr
+  of the absolute path.  If relative-paths is true, this will be a
+  function call to convert a buildout-relative path to an absolute path;
+  it requires that ``${python-relative-path-setup}`` be included earlier
+  in the template.
+
+- shell-path: Converts the path to a shell expression for the path.  Only
+  POSIX is supported at this time.  If buildout's relative-paths option
+  is false, this will simply be the absolute path.  If relative-paths is
+  true, this will be an expression to convert a buildout-relative path
+  to an absolute path; it requires that ``${shell-relative-path-setup}``
+  be included earlier in the template.
+
+Combining the three advanced features described so far, then, if the
+buildout relative-paths option were false, we were in a POSIX system, and
+the sample buildout were in the root of the system, the template
+expression ``${buildout:bin-directory/data/initial.csv|path-repr}``
+would result in ``'/sample-buildout/bin/data/initial.csv'``.
+
+Here's a real, working example of the string method filters.  We'll have
+examples of the path filters in the `Working with Paths`_ section.
+
+    >>> update_file(sample_buildout, 'helloworld.txt.in',
+    ... """
+    ... HELLO ${world|upper}!
+    ... hello ${world|lower}.
+    ... ${name|title} and the Chocolate Factory
+    ... ${sentence|capitalize}
+    ... """)
+
+    >>> write(sample_buildout, 'buildout.cfg',
+    ... """
+    ... [buildout]
+    ... parts = message
+    ...
+    ... [message]
+    ... recipe = z3c.recipe.filetemplate
+    ... files = helloworld.txt
+    ... world = Philipp
+    ... name = willy wonka
+    ... sentence = that is a good book.
+    ... """)
+
+    >>> print system(buildout)
+    Uninstalling message.
+    Installing message.
+
+    >>> cat(sample_buildout, 'helloworld.txt') # doctest: +ELLIPSIS
+    HELLO PHILIPP!
+    hello philipp.
+    Willy Wonka and the Chocolate Factory
+    That is a good book.
+
+Sharing Variables
 =================
 
-The recipe allows extending one or more sections, to decrease repetition, using
-the ``extends`` option.  For instance, consider the following buildout.
+The recipe allows extending one or more sections, to decrease
+repetition, using the ``extends`` option.  For instance, consider the
+following buildout.
 
     >>> write(sample_buildout, 'buildout.cfg',
     ... """
     ... [buildout]
     ... parts = message
-    ... 
+    ...
     ... [template_defaults]
     ... mygreeting = Hi
     ... myaudience = World
-    ... 
+    ...
     ... [message]
     ... recipe = z3c.recipe.filetemplate
     ... files = helloworld.txt
@@ -255,7 +387,7 @@
 section, and overwritten locally.  A template of
 ``${mygreeting}, ${myaudience}!``...
 
-    >>> write(sample_buildout, 'helloworld.txt.in',
+    >>> update_file(sample_buildout, 'helloworld.txt.in',
     ... """
     ... ${mygreeting}, ${myaudience}!
     ... """)
@@ -269,32 +401,287 @@
     >>> cat(sample_buildout, 'helloworld.txt')
     Hi, everybody!
 
-Specifying paths
-================
+Defining options in Python
+==========================
 
-You can specify eggs and extra-paths in the recipe.  If you do, three
-predefined options will be available in the recipe's options for the template.
-If "paths" are the non-zip paths, and "all_paths" are all paths, then the
-options would be defined roughly as given here:
+You can specify that certain variables should be interpreted as Python using
+``interpreted-options``.  This takes zero or more lines.  Each line should
+specify an option.  It can define immediately (see ``silly-range`` in
+the example below) or point to an option to be interepreted, which can
+be useful if you want to define a multi-line expression (see
+``first-interpreted-option`` and ``message-reversed-is-egassem``).
 
-``os-paths``
-  ``(os.pathsep).join(paths)``
-  
-``string-paths``
-  ``', '.join(repr(p) for p in all_paths)``
+    >>> write(sample_buildout, 'buildout.cfg',
+    ... """
+    ... [buildout]
+    ... parts = message
+    ...
+    ... [message]
+    ... recipe = z3c.recipe.filetemplate
+    ... files = helloworld.txt
+    ... interpreted-options = silly-range = repr(range(5))
+    ...                       first-interpreted-option
+    ...                       message-reversed-is-egassem
+    ... first-interpreted-option =
+    ...     options['interpreted-options'].splitlines()[0].strip()
+    ... message-reversed-is-egassem=
+    ...     ''.join(
+    ...         reversed(
+    ...             buildout['buildout']['parts']))
+    ... not-interpreted=hello world
+    ... """)
 
-``space-paths``
-  ``' '.join(paths)``
+    >>> update_file(sample_buildout, 'helloworld.txt.in', """\
+    ... ${not-interpreted}!
+    ... silly-range: ${silly-range}
+    ... first-interpreted-option: ${first-interpreted-option}
+    ... message-reversed-is-egassem: ${message-reversed-is-egassem}
+    ... """)
 
-For instance, consider this example.
+    >>> print system(buildout)
+    Uninstalling message.
+    Installing message.
 
+    >>> cat(sample_buildout, 'helloworld.txt') # doctest:+ELLIPSIS
+    hello world!
+    silly-range: [0, 1, 2, 3, 4]
+    first-interpreted-option: silly-range = repr(range(5))
+    message-reversed-is-egassem: egassem
+
+Working with Paths
+==================
+
+We've already mentioned how to handle buildout's relative-paths option
+in the discussion of filters.  This section has some concrete examples
+and discussion of that.  It also introduces how to get a set of paths
+from specifying dependencies.
+
+Here are concrete examples of the path-repr and shell-path filters.
+We'll show results when relative-paths is true and when it is false.
+
+------------------------------
+Demonstration of ``path-repr``
+------------------------------
+
+Let's say we want to make a custom Python script in the bin directory.
+It will print some information from a file in a ``data`` directory
+within the buildout root.  Here's the template.
+
+    >>> write(sample_buildout, 'template', 'bin', 'dosomething.py.in', '''\
+    ... #!${buildout:executable}
+    ... ${python-relative-path-setup}
+    ... f = open(${buildout:directory/data/info.csv|path-repr})
+    ... print f.read()
+    ... ''')
+    >>> os.chmod(
+    ...     os.path.join(
+    ...         sample_buildout, 'template', 'bin', 'dosomething.py.in'),
+    ...     0711)
+
+If we evaluate that template with relative-paths set to false, the results
+shouldn't be too surprising.
+
     >>> write(sample_buildout, 'buildout.cfg',
     ... """
     ... [buildout]
     ... parts = message
-    ... 
+    ...
     ... [message]
     ... recipe = z3c.recipe.filetemplate
+    ... source-directory = template
+    ... """)
+
+    >>> print system(buildout)
+    Uninstalling message.
+    Installing message.
+
+    >>> cat(sample_buildout, 'bin', 'dosomething.py') # doctest: +ELLIPSIS
+    #!...
+    <BLANKLINE>
+    f = open('/.../sample-buildout/data/info.csv')
+    print f.read()
+
+``${python-relative-path-setup}`` evaluated to an empty string.  The path
+is absolute and quoted.
+
+If we evaluate it with relative-paths set to true, the results are much...
+bigger.
+
+    >>> write(sample_buildout, 'buildout.cfg',
+    ... """
+    ... [buildout]
+    ... parts = message
+    ... relative-paths = true
+    ...
+    ... [message]
+    ... recipe = z3c.recipe.filetemplate
+    ... source-directory = template
+    ... """)
+
+    >>> print system(buildout)
+    Uninstalling message.
+    Installing message.
+
+    >>> cat(sample_buildout, 'bin', 'dosomething.py') # doctest: +ELLIPSIS
+    #!...
+    import os, imp
+    # Get path to this file.
+    if __name__ == '__main__':
+        _z3c_recipe_filetemplate_filename = __file__
+    else:
+        # If this is an imported module, we want the location of the .py
+        # file, not the .pyc, because the .py file may have been symlinked.
+        _z3c_recipe_filetemplate_filename = imp.find_module(__name__)[1]
+    # Get the full, non-symbolic-link directory for this file.
+    _z3c_recipe_filetemplate_base = os.path.dirname(
+        os.path.abspath(os.path.realpath(_z3c_recipe_filetemplate_filename)))
+    # Ascend to buildout root.
+    _z3c_recipe_filetemplate_base = os.path.dirname(
+        _z3c_recipe_filetemplate_base)
+    def _z3c_recipe_filetemplate_path_repr(path):
+        "Return absolute version of buildout-relative path."
+        return os.path.join(_z3c_recipe_filetemplate_base, path)
+    <BLANKLINE>
+    f = open(_z3c_recipe_filetemplate_path_repr('data/info.csv'))
+    print f.read()
+
+That's quite a bit of code.  You might wonder why we don't just use '..' for
+parent directories.  The reason is that we want our scripts to be usable
+from any place on the filesystem.  If we used '..' to construct paths
+relative to the generated file, then the paths would only work from
+certain directories.
+
+So that's how path-repr works.  It can really come in handy if you want
+to support relative paths in buildout.  Now let's look at the shell-path
+filter.
+
+-------------------------------
+Demonstration of ``shell-path``
+-------------------------------
+
+Maybe you want to write some shell scripts.  The shell-path filter will help
+you support buildout relative-paths fairly painlessly.
+
+Right now, only POSIX is supported with the shell-path filter, as mentioned
+before.
+
+Usage is very similar to the ``path-repr`` filter.  You need to include
+``${shell-relative-path-setup}`` before you use it, just as you include
+``${python-relative-path-setup}`` before using ``path-repr``.
+
+Let's say we want to make a custom shell script in the bin directory.
+It will print some information from a file in a ``data`` directory
+within the buildout root.  Here's the template.
+
+    >>> write(sample_buildout, 'template', 'bin', 'dosomething.sh.in', '''\
+    ... #!/bin/sh
+    ... ${shell-relative-path-setup}
+    ... cat ${buildout:directory/data/info.csv|shell-path}
+    ... ''')
+    >>> os.chmod(
+    ...     os.path.join(
+    ...         sample_buildout, 'template', 'bin', 'dosomething.sh.in'),
+    ...     0711)
+
+If relative-paths is set to false (the default), the results are simple.
+
+    >>> write(sample_buildout, 'buildout.cfg',
+    ... """
+    ... [buildout]
+    ... parts = message
+    ...
+    ... [message]
+    ... recipe = z3c.recipe.filetemplate
+    ... source-directory = template
+    ... """)
+
+    >>> print system(buildout)
+    Uninstalling message.
+    Installing message.
+
+    >>> cat(sample_buildout, 'bin', 'dosomething.sh') # doctest: +ELLIPSIS
+    #!/bin/sh
+    <BLANKLINE>
+    cat /.../sample-buildout/data/info.csv
+
+``${shell-relative-path-setup}`` evaluated to an empty string.  The path
+is absolute.
+
+Now let's look at the larger code when relative-paths is set to true.
+
+    >>> write(sample_buildout, 'buildout.cfg',
+    ... """
+    ... [buildout]
+    ... parts = message
+    ... relative-paths = true
+    ...
+    ... [message]
+    ... recipe = z3c.recipe.filetemplate
+    ... source-directory = template
+    ... """)
+
+    >>> print system(buildout)
+    Uninstalling message.
+    Installing message.
+
+    >>> cat(sample_buildout, 'bin', 'dosomething.sh') # doctest: +ELLIPSIS
+    #!/bin/sh
+    # Get full, non-symbolic-link path to this file.
+    Z3C_RECIPE_FILETEMPLATE_FILENAME=`\
+        readlink -f "$0" 2>/dev/null || \
+        realpath "$0" 2>/dev/null || \
+        type -P "$0" 2>/dev/null`
+    # Get directory of file.
+    Z3C_RECIPE_FILETEMPLATE_BASE=`dirname ${Z3C_RECIPE_FILETEMPLATE_FILENAME}`
+    # Ascend to buildout root.
+    Z3C_RECIPE_FILETEMPLATE_BASE=`dirname ${Z3C_RECIPE_FILETEMPLATE_BASE}`
+    <BLANKLINE>
+    cat "$Z3C_RECIPE_FILETEMPLATE_BASE"/data/info.csv
+
+As with the Python code, we don't just use '..' for
+parent directories because we want our scripts to be usable
+from any place on the filesystem.
+
+----------------------------------
+Getting Arbitrary Dependency Paths
+----------------------------------
+
+You can specify ``eggs`` and ``extra-paths`` in the recipe.  The
+mechanism is the same as the one provided by the zc.recipe.egg, so
+pertinent options such as find-links and index are available.
+
+If you do, the paths for the dependencies will be calculated.  They will
+be available as a list in the namespace of the interpreted options as
+``paths``.  Also, three predefined options will be available in the
+recipe's options for the template.
+
+If ``paths`` are the paths, ``shell_path`` is the ``shell-path`` filter, and
+``path_repr`` is the ``path-repr`` filter, then the pre-defined options
+would be defined roughly as given here:
+
+``os-paths`` (for shell scripts)
+  ``(os.pathsep).join(shell_path(path) for path in paths)``
+
+``string-paths`` (for Python scripts)
+  ``',\n    '.join(path_repr(path) for path in paths)``
+
+``space-paths`` (for shell scripts)
+  ``' '.join(shell_path(path) for path in paths)``
+
+Therefore, if you want to support the relative-paths option, you should
+include ``${shell-relative-path-setup}`` (for ``os-paths`` and
+``space-paths``) or ``${python-relative-path-setup}`` (for ``string-paths``)
+as appropriate at the top of your template.
+
+Let's consider a simple example.
+
+    >>> write(sample_buildout, 'buildout.cfg',
+    ... """
+    ... [buildout]
+    ... parts = message
+    ...
+    ... [message]
+    ... recipe = z3c.recipe.filetemplate
     ... files = helloworld.txt
     ... eggs = demo<0.3
     ...
@@ -302,6 +689,7 @@
     ... index = %(server)s/index
     ... """ % dict(server=link_server))
 
+The relative-paths option is false, the default.
 
     >>> write(sample_buildout, 'helloworld.txt.in',
     ... """
@@ -327,21 +715,23 @@
     >>> cat(sample_buildout, 'helloworld.txt') # doctest:+ELLIPSIS
     Hello!  Here are the paths for the demo<0.3 eggs.
     OS paths:
-    .../eggs/demo-0.2...egg:.../eggs/demoneeded-1.2c1...egg
+    /.../eggs/demo-0.2...egg:/.../eggs/demoneeded-1.2c1...egg
     ---
     String paths:
-    '.../eggs/demo-0.2...egg', '.../eggs/demoneeded-1.2c1...egg'
+    '/.../eggs/demo-0.2...egg',
+    '/.../eggs/demoneeded-1.2c1...egg'
     ---
     Space paths:
-    .../eggs/demo-0.2...egg .../eggs/demoneeded-1.2c1...egg
+    /.../eggs/demo-0.2...egg /.../eggs/demoneeded-1.2c1...egg
 
-You can specify extra-paths as well, which will go at the end of the egg paths.
+You can specify extra-paths as well, which will go at the end of the egg
+paths.
 
     >>> write(sample_buildout, 'buildout.cfg',
     ... """
     ... [buildout]
     ... parts = message
-    ... 
+    ...
     ... [message]
     ... recipe = z3c.recipe.filetemplate
     ... files = helloworld.txt
@@ -359,70 +749,116 @@
     >>> cat(sample_buildout, 'helloworld.txt') # doctest:+ELLIPSIS
     Hello!  Here are the paths for the demo<0.3 eggs.
     OS paths:
-    ...demo...:...demoneeded...:.../sample-buildout/foo
+    /...demo...:/...demoneeded...:/.../sample-buildout/foo
     ---
     String paths:
-    '...demo...', '...demoneeded...', '.../sample-buildout/foo'
+    '/...demo...',
+    '/...demoneeded...',
+    '/.../sample-buildout/foo'
     ---
     Space paths:
-    ...demo... ...demoneeded... .../sample-buildout/foo
+    /...demo... /...demoneeded... .../sample-buildout/foo
 
-Defining options in Python
-==========================
+To emphasize the effect of the relative-paths option, let's see what it looks
+like when we set relative-paths to True.
 
-You can specify that certain variables should be interpreted as Python using
-``interpreted-options``.  This takes zero or more lines.  Each line should
-specify an option.  It can define immediately (see ``duplicate-os-paths``,
-``foo-paths``, and ``silly-range`` in the example below) or point to an option
-to be interepreted, which can be useful if you want to define a
-multi-line expression (see ``first-interpreted-option`` and
-``message-reversed-is-egassem``).
-
     >>> write(sample_buildout, 'buildout.cfg',
     ... """
     ... [buildout]
     ... parts = message
-    ... 
+    ... relative-paths = true
+    ...
     ... [message]
     ... recipe = z3c.recipe.filetemplate
     ... files = helloworld.txt
     ... eggs = demo<0.3
-    ... interpreted-options = duplicate-os-paths=(os.pathsep).join(paths)
-    ...                       foo-paths='FOO'.join(all_paths)
-    ...                       silly-range = repr(range(5))
-    ...                       first-interpreted-option
-    ...                       message-reversed-is-egassem
-    ... first-interpreted-option = 
-    ...     options['interpreted-options'].split()[0].strip()
-    ... message-reversed-is-egassem=
-    ...     ''.join(
-    ...         reversed(
-    ...             buildout['buildout']['parts']))
-    ... not-interpreted=hello world
+    ... extra-paths = ${buildout:directory}/foo
     ...
     ... find-links = %(server)s
     ... index = %(server)s/index
     ... """ % dict(server=link_server))
 
-    >>> write(sample_buildout, 'helloworld.txt.in',
-    ... """
-    ... ${not-interpreted}!
-    ... duplicate-os-paths: ${duplicate-os-paths}
-    ... foo-paths: ${foo-paths}
-    ... silly-range: ${silly-range}
-    ... first-interpreted-option: ${first-interpreted-option}
-    ... message-reversed-is-egassem: ${message-reversed-is-egassem}
-    ... """)
-
     >>> print system(buildout)
     Uninstalling message.
     Installing message.
 
     >>> cat(sample_buildout, 'helloworld.txt') # doctest:+ELLIPSIS
-    hello world!
-    duplicate-os-paths: ...demo-0.2...egg:...demoneeded-1.2c1...egg
-    foo-paths: ...demo-0.2...eggFOO...demoneeded-1.2c1...egg
-    silly-range: [0, 1, 2, 3, 4]
-    first-interpreted-option: duplicate-os-paths=(os.pathsep).join(paths)
-    message-reversed-is-egassem: egassem
+    Hello!  Here are the paths for the demo<0.3 eggs.
+    OS paths:
+    "$Z3C_RECIPE_FILETEMPLATE_BASE"/eggs/demo-0.2-py...egg:"$Z3C_RECIPE_FILETEMPLATE_BASE"/eggs/demoneeded-1.2c1-py...egg:"$Z3C_RECIPE_FILETEMPLATE_BASE"/foo
+    ---
+    String paths:
+    _z3c_recipe_filetemplate_path_repr('eggs/demo-0.2-py...egg'),
+    _z3c_recipe_filetemplate_path_repr('eggs/demoneeded-1.2c1-py...egg'),
+    _z3c_recipe_filetemplate_path_repr('foo')
+    ---
+    Space paths:
+    "$Z3C_RECIPE_FILETEMPLATE_BASE"/eggs/demo-0.2-py...egg "$Z3C_RECIPE_FILETEMPLATE_BASE"/eggs/demoneeded-1.2c1-py...egg "$Z3C_RECIPE_FILETEMPLATE_BASE"/foo
 
+
+Remember, your script won't really work unless you include
+``${shell-relative-path-setup}`` (for ``os-paths`` and ``space-paths``)
+or ``${python-relative-path-setup}`` (for ``string-paths``) as
+appropriate at the top of your template.
+
+Getting Dependency Paths from ``zc.recipe.egg``
+-----------------------------------------------
+
+You can get the ``eggs`` and ``extra-paths`` from another section using
+zc.recipe.egg by using the ``extends`` option from the `Sharing Variables`_
+section above.  Then you can use the template options described above to
+build your paths in your templates.
+
+Getting Dependency Paths from ``z3c.recipe.scripts``
+----------------------------------------------------
+
+If, like the Launchpad project, you are using Gary Poster's unreleased
+package ``z3c.recipe.scripts`` to generate your scripts, and you want to
+have your scripts use the same Python environment as generated by that
+recipe, you can just use the path-repr and shell-path filters with standard
+buildout directories.  Here is an example buildout.cfg.
+
+::
+
+    [buildout]
+    parts = scripts message
+    relative-paths = true
+
+    [scripts]
+    recipe = z3c.recipe.scripts
+    eggs = demo<0.3
+
+    [message]
+    recipe = z3c.recipe.filetemplate
+    files = helloworld.py
+
+Then the template to use this would want to simply put
+``${scripts:parts-directory|path-repr}`` at the beginning of Python's path.
+
+You can do this for subprocesses with PYTHONPATH.
+
+    ${python-relative-path-setup}
+    import os
+    import subprocess
+    env = os.environ.copy()
+    env['PYTHONPATH'] = ${scripts:parts-directory|path-repr}
+    subprocess.call('myscript', env=env)
+
+That's it.
+
+Similarly, here's an approach to making a script that will have the
+right environment.  You want to put the parts directory of the
+z3c.recipe.scripts section in the sys.path before site.py is loaded.
+This is usually handled by z3c.recipe.scripts itself, but sometimes you
+may want to write Python scripts in your template for some reason.
+
+    #!/usr/bin/env python -S
+    ${python-relative-path-setup}
+    import sys
+    sys.path.insert(0, ${scripts:parts-directory|path-repr})
+    import site
+    # do stuff...
+
+If you do this for many scripts, put this entire snippet in an option in the
+recipe and use this snippet as a single substitution in the top of your
+scripts.

Modified: z3c.recipe.filetemplate/trunk/z3c/recipe/filetemplate/__init__.py
===================================================================
--- z3c.recipe.filetemplate/trunk/z3c/recipe/filetemplate/__init__.py	2010-04-21 17:13:11 UTC (rev 111209)
+++ z3c.recipe.filetemplate/trunk/z3c/recipe/filetemplate/__init__.py	2010-04-21 17:55:06 UTC (rev 111210)
@@ -29,10 +29,15 @@
 
 class FileTemplate(object):
 
+    filters = {}
+    dynamic_options = {}
+
     def __init__(self, buildout, name, options):
         self.buildout = buildout
         self.name = name
         self.options = options
+        self.buildout_root = zc.buildout.easy_install.realpath(
+            buildout['buildout']['directory'])
         self.logger=logging.getLogger(self.name)
         # get defaults from extended sections
         defaults = {}
@@ -42,23 +47,33 @@
             defaults.update(self.buildout[section_name])
         for key, value in defaults.items():
             self.options.setdefault(key, value)
+        relative_paths = self.options.setdefault(
+            'relative-paths',
+            buildout['buildout'].get('relative-paths', 'false')
+            )
+        if relative_paths not in ('true', 'false'):
+            self._user_error(
+                'The relative-paths option must have the value of '
+                'true or false.')
+        self.relative_paths = relative_paths = (relative_paths == 'true')
+        self.paths = paths = []
         # set up paths for eggs, if given
-        if 'eggs' in self.options:
-            self.eggs = zc.recipe.egg.Scripts(buildout, name, options)
-            orig_distributions, ws = self.eggs.working_set()
-            # we want ws, eggs.extra_paths, eggs._relative_paths
-            all_paths = [
+        if 'eggs' in options:
+            eggs = zc.recipe.egg.Scripts(buildout, name, options)
+            orig_distributions, ws = eggs.working_set()
+            paths.extend(
                 zc.buildout.easy_install.realpath(dist.location)
-                for dist in ws]
-            all_paths.extend(
+                for dist in ws)
+            paths.extend(
                 zc.buildout.easy_install.realpath(path)
-                for path in self.eggs.extra_paths)
+                for path in eggs.extra_paths)
         else:
-            all_paths = []
-        paths = [path for path in all_paths if not path.endswith('.zip')]
-        self.options['os-paths'] = (os.pathsep).join(paths)
-        self.options['string-paths'] = ', '.join(repr(p) for p in all_paths)
-        self.options['space-paths'] = ' '.join(paths)
+            paths.extend(
+                os.path.join(buildout.options['directory'], p.strip())
+                for p in options.get('extra-paths', '').split('\n')
+                if p.strip()
+                )
+        options['_paths'] = '\n'.join(paths)
         # get and check the files to be created
         self.filenames = self.options.get('files', '*').split()
         self.source_dir = self.options.get('source-directory', '').strip()
@@ -153,7 +168,7 @@
         if interpreted:
             globs = {'__builtins__': __builtins__, 'os': os, 'sys': sys}
             locs = {'name': name, 'options': options, 'buildout': buildout,
-                    'paths': paths, 'all_paths': all_paths}
+                    'paths': paths, 'all_paths': paths}
             for value in interpreted.split('\n'):
                 if value:
                     value = value.split('=', 1)
@@ -179,6 +194,7 @@
                             'a %s.',
                             key, expression, evaluated, type(evaluated))
                     options[key] = evaluated
+
     def _user_error(self, msg, *args):
         msg = msg % args
         self.logger.error(msg)
@@ -195,17 +211,21 @@
                 'Destinations already exist: %s. Please make sure that '
                 'you really want to generate these automatically.  Then '
                 'move them away.', ', '.join(already_exists))
+        self.seen = []
+        # We throw ``seen`` away right now, but could move template
+        # processing up to __init__ if valuable.  That would mean that
+        # templates would be rewritten even if a value in another
+        # section had been referenced; however, it would also mean that
+        # __init__ would do virtually all of the work, with install only
+        # doing the writing.
         for rel_path, last_mod, st_mode in self.actions:
             source = os.path.join(self.source_dir, rel_path)
             dest = os.path.join(self.destination_dir, rel_path[:-3])
             mode=stat.S_IMODE(st_mode)
-            template=open(source).read()
-            template=re.sub(r"\$\{([^:]+?)\}", r"${%s:\1}" % self.name,
-                            template)
-            self._create_paths(os.path.dirname(dest))
             # we process the file first so that it won't be created if there
             # is a problem.
-            processed = self.options._sub(template, [])
+            processed = Template(source, dest, self).substitute()
+            self._create_paths(os.path.dirname(dest))
             result=open(dest, "wt")
             result.write(processed)
             result.close()
@@ -219,5 +239,313 @@
             os.mkdir(path)
             self.options.created(path)
 
+    def _call_and_log(self, callable, args, message_generator):
+        try:
+            return callable(*args)
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except:
+            # Argh.  Would like to raise wrapped exception.
+            colno, lineno = self.get_colno_lineno(start)
+            msg = message_generator(lineno, colno)
+            self.logger.error(msg, exc_info=True)
+            raise
+
     def update(self):
         pass
+
+
+class Template:
+    # Heavily hacked from--"inspired by"?--string.Template
+    pattern = re.compile(r"""
+    \$(?:
+      \${(?P<escaped>[^}]*)} |           # Escape sequence of two delimiters.
+
+      {((?P<section>[-a-z0-9 ._]+):)?    # Optional section name.
+       (?P<option>[-a-z0-9 ._]+)         # Required option name.
+       (?P<path_extension>/[^|}]+/?)?    # Optional path extensions.
+       ([ ]*(?P<filters>(\|[ ]*[-a-z0-9._]+[ ]*)+))?
+                                         # Optional filters.
+        } |
+
+      {(?P<invalid>[^}]*})               # Other ill-formed delimiter exprs.
+    )
+    """, re.IGNORECASE | re.VERBOSE)
+
+    def __init__(self, source, destination, recipe):
+        self.source = source
+        self.destination = zc.buildout.easy_install.realpath(destination)
+        self.recipe = recipe
+        self.template = open(source).read()
+
+    def get_colno_lineno(self, i):
+        lines = self.template[:i].splitlines(True)
+        if not lines:
+            colno = 1
+            lineno = 1
+        else:
+            colno = len(lines[-1]) + 1
+            lineno = len(lines)
+        return colno, lineno
+
+    def _get(self, section, option, start):
+        if section is None:
+            section = self.recipe.name # This sets up error messages properly.
+        if section == self.recipe.name:
+            factory = self.recipe.dynamic_options.get(option)
+            if factory is not None:
+                return self.recipe._call_and_log(
+                    factory, (self, start, option),
+                    lambda lineno, colno: (
+                        'Dynamic option %r in line %d, col %d of %s '
+                        'crashed.') % (option, lineno, colno, self.source))
+            # else...
+            options = self.recipe.options
+        elif section in self.recipe.buildout:
+            options = self.recipe.buildout[section]
+        else:
+            value = options = None
+        if options is not None:
+            value = options.get(option, None, self.recipe.seen)
+        if value is None:
+            colno, lineno = self.get_colno_lineno(start)
+            raise zc.buildout.buildout.MissingOption(
+                "Option '%s:%s', referenced in line %d, col %d of %s, "
+                "does not exist." %
+                (section, option, lineno, colno, self.source))
+        return value
+
+    def substitute(self):
+        def convert(mo):
+            start = mo.start()
+            # Check the most common path first.
+            option = mo.group('option')
+            if option is not None:
+                section = mo.group('section')
+                val = self._get(section, option, start)
+                path_extension = mo.group('path_extension')
+                filters = mo.group('filters')
+                if path_extension is not None:
+                    val = os.path.join(val, *path_extension.split('/')[1:])
+                if filters is not None:
+                    for filter_name in filters.split('|')[1:]:
+                        filter_name = filter_name.strip()
+                        filter = self.recipe.filters.get(filter_name)
+                        if filter is None:
+                            colno, lineno = self.get_colno_lineno(start)
+                            raise ValueError(
+                                'Unknown filter %r '
+                                'in line %d, col %d of %s' %
+                                (filter_name, lineno, colno, self.source))
+                        val = self.recipe._call_and_log(
+                            filter, (val, self, start, filter_name),
+                            lambda lineno, colno: (
+                                'Filter %r in line %d, col %d of %s '
+                                'crashed processing value %r') % (
+                                filter_name, lineno, colno, self.source, val))
+                # We use this idiom instead of str() because the latter will
+                # fail if val is a Unicode containing non-ASCII characters.
+                return '%s' % (val,)
+            escaped = mo.group('escaped')
+            if escaped is not None:
+                return '${%s}' % (escaped,)
+            invalid = mo.group('invalid')
+            if invalid is not None:
+                colno, lineno = self.get_colno_lineno(mo.start('invalid'))
+                raise ValueError(
+                    'Invalid placeholder %r in line %d, col %d of %s' %
+                    (mo.group('invalid'), lineno, colno, self.source))
+            raise ValueError('Unrecognized named group in pattern',
+                             self.pattern) # programmer error, AFAICT
+        return self.pattern.sub(convert, self.template)
+
+
+############################################################################
+# Filters
+def filter(func):
+    "Helper function to register filter functions."
+    FileTemplate.filters[func.__name__.replace('_', '-')] = func
+    return func
+
+ at filter
+def capitalize(val, template, start, filter):
+    return val.capitalize()
+
+ at filter
+def title(val, template, start, filter):
+    return val.title()
+
+ at filter
+def upper(val, template, start, filter):
+    return val.upper()
+
+ at filter
+def lower(val, template, start, filter):
+    return val.lower()
+
+ at filter
+def path_repr(val, template, start, filter):
+    # val is a path.
+    return _maybe_relativize(
+        val, template,
+        lambda p: "_z3c_recipe_filetemplate_path_repr(%r)" % (p,),
+        repr)
+
+ at filter
+def shell_path(val, template, start, filter):
+    # val is a path.
+    return _maybe_relativize(
+        val, template,
+        lambda p: '"$Z3C_RECIPE_FILETEMPLATE_BASE"/%s' % (p,),
+        lambda p: p)
+
+# Helpers hacked from zc.buildout.easy_install.
+def _maybe_relativize(path, template, relativize, absolutize):
+    path = zc.buildout.easy_install.realpath(path)
+    if template.recipe.relative_paths:
+        buildout_root = template.recipe.buildout_root
+        if path == buildout_root:
+            return relativize(os.curdir)
+        destination = template.destination
+        common = os.path.dirname(os.path.commonprefix([path, destination]))
+        if (common == buildout_root or
+            common.startswith(os.path.join(buildout_root, ''))
+            ):
+            return relativize(_relative_path(common, path))
+    return absolutize(path)
+
+def _relative_path(common, path):
+    """Return the relative path from ``common`` to ``path``.
+
+    This is a helper for _relativitize, which is a helper to
+    _relative_path_and_setup.
+    """
+    r = []
+    while 1:
+        dirname, basename = os.path.split(path)
+        r.append(basename)
+        if dirname == common:
+            break
+        assert dirname != path, "dirname of %s is the same" % dirname
+        path = dirname
+    r.reverse()
+    return os.path.join(*r)
+
+
+############################################################################
+# Dynamic options
+def dynamic_option(func):
+    "Helper function to register dynamic options."
+    FileTemplate.dynamic_options[func.__name__.replace('_', '-')] = func
+    return func
+
+ at dynamic_option
+def os_paths(template, start, name):
+    return os.pathsep.join(
+        shell_path(path, template, start, 'os-paths')
+        for path in template.recipe.paths)
+
+ at dynamic_option
+def string_paths(template, start, name):
+    colno, lineno = template.get_colno_lineno(start)
+    separator = ',\n' + ((colno - 1) * ' ')
+    return separator.join(
+        path_repr(path, template, start, 'string-paths')
+        for path in template.recipe.paths)
+
+ at dynamic_option
+def space_paths(template, start, name):
+    return ' '.join(
+        shell_path(path, template, start, 'space-paths')
+        for path in template.recipe.paths)
+
+ at dynamic_option
+def shell_relative_path_setup(template, start, name):
+    if template.recipe.relative_paths:
+        depth = _relative_depth(
+            template.recipe.buildout['buildout']['directory'],
+            template.destination)
+        value = SHELL_RELATIVE_PATH_SETUP
+        if depth:
+            value += '# Ascend to buildout root.\n'
+            value += depth * SHELL_DIRNAME
+        else:
+            value += '# This is the buildout root.\n'
+        return value
+    else:
+        return ''
+
+SHELL_RELATIVE_PATH_SETUP = '''\
+# Get full, non-symbolic-link path to this file.
+Z3C_RECIPE_FILETEMPLATE_FILENAME=`\\
+    readlink -f "$0" 2>/dev/null || \\
+    realpath "$0" 2>/dev/null || \\
+    type -P "$0" 2>/dev/null`
+# Get directory of file.
+Z3C_RECIPE_FILETEMPLATE_BASE=`dirname ${Z3C_RECIPE_FILETEMPLATE_FILENAME}`
+'''
+
+SHELL_DIRNAME = '''\
+Z3C_RECIPE_FILETEMPLATE_BASE=`dirname ${Z3C_RECIPE_FILETEMPLATE_BASE}`
+'''
+
+ at dynamic_option
+def python_relative_path_setup(template, start, name):
+    if template.recipe.relative_paths:
+        depth = _relative_depth(
+            template.recipe.buildout['buildout']['directory'],
+            template.destination)
+        value = PYTHON_RELATIVE_PATH_SETUP_START
+        if depth:
+            value += '# Ascend to buildout root.\n'
+            value += depth * PYTHON_DIRNAME
+        else:
+            value += '# This is the buildout root.\n'
+        value += PYTHON_RELATIVE_PATH_SETUP_END
+        return value
+    else:
+        return ''
+
+PYTHON_RELATIVE_PATH_SETUP_START = '''\
+import os, imp
+# Get path to this file.
+if __name__ == '__main__':
+    _z3c_recipe_filetemplate_filename = __file__
+else:
+    # If this is an imported module, we want the location of the .py
+    # file, not the .pyc, because the .py file may have been symlinked.
+    _z3c_recipe_filetemplate_filename = imp.find_module(__name__)[1]
+# Get the full, non-symbolic-link directory for this file.
+_z3c_recipe_filetemplate_base = os.path.dirname(
+    os.path.abspath(os.path.realpath(_z3c_recipe_filetemplate_filename)))
+'''
+
+PYTHON_DIRNAME = '''\
+_z3c_recipe_filetemplate_base = os.path.dirname(
+    _z3c_recipe_filetemplate_base)
+'''
+
+PYTHON_RELATIVE_PATH_SETUP_END = '''\
+def _z3c_recipe_filetemplate_path_repr(path):
+    "Return absolute version of buildout-relative path."
+    return os.path.join(_z3c_recipe_filetemplate_base, path)
+'''
+
+def _relative_depth(common, path):
+    # Helper ripped from zc.buildout.easy_install.
+    """Return number of dirs separating ``path`` from ancestor, ``common``.
+
+    For instance, if path is /foo/bar/baz/bing, and common is /foo, this will
+    return 2--in UNIX, the number of ".." to get from bing's directory
+    to foo.
+    """
+    n = 0
+    while 1:
+        dirname = os.path.dirname(path)
+        if dirname == path:
+            raise AssertionError("dirname of %s is the same" % dirname)
+        if dirname == common:
+            break
+        n += 1
+        path = dirname
+    return n

Modified: z3c.recipe.filetemplate/trunk/z3c/recipe/filetemplate/tests.py
===================================================================
--- z3c.recipe.filetemplate/trunk/z3c/recipe/filetemplate/tests.py	2010-04-21 17:13:11 UTC (rev 111209)
+++ z3c.recipe.filetemplate/trunk/z3c/recipe/filetemplate/tests.py	2010-04-21 17:55:06 UTC (rev 111210)
@@ -12,12 +12,32 @@
 #
 ##############################################################################
 
+import os
+import time
 import zc.buildout.testing
 import zc.buildout.tests
 from zope.testing import doctest
 
+
+def update_file(dir, *args):
+    """Update a file.
+
+    Make sure that the mtime of the file is updated so that buildout notices
+    the changes.  The resolution of mtime is system dependent, so we keep
+    trying to write until mtime has actually changed."""
+    path = os.path.join(dir, *(args[:-1]))
+    original = os.stat(path).st_mtime
+    while True:
+        f = open(path, 'w')
+        f.write(args[-1])
+        f.flush()
+        if os.stat(path).st_mtime != original:
+            break
+        time.sleep(0.2)
+
 def setUp(test):
     zc.buildout.tests.easy_install_SetUp(test)
+    test.globs['update_file'] = update_file
     zc.buildout.testing.install_develop('z3c.recipe.filetemplate', test)
 
 def test_suite():

Modified: z3c.recipe.filetemplate/trunk/z3c/recipe/filetemplate/tests.txt
===================================================================
--- z3c.recipe.filetemplate/trunk/z3c/recipe/filetemplate/tests.txt	2010-04-21 17:13:11 UTC (rev 111209)
+++ z3c.recipe.filetemplate/trunk/z3c/recipe/filetemplate/tests.txt	2010-04-21 17:55:06 UTC (rev 111210)
@@ -14,7 +14,7 @@
     ... """
     ... Hello ${world}!
     ... """)
-  
+
     >>> write(sample_buildout, 'goodbyeworld.txt.in',
     ... """
     ... Goodbye ${world}!
@@ -56,7 +56,7 @@
     ... files = /etc/passwd.in
     ... root = me
     ... """)
-  
+
     >>> print system(buildout)
     evil: /etc/passwd.in is an absolute path. Paths must be relative to the buildout directory.
     While:
@@ -80,7 +80,7 @@
     ... recipe = z3c.recipe.filetemplate
     ... files = doesntexist
     ... """)
-  
+
     >>> print system(buildout)
     notthere: No template found for these file names: doesntexist.in
     While:
@@ -99,12 +99,12 @@
     ... """
     ... I'm already here
     ... """)
-  
+
     >>> write(sample_buildout, 'alreadyhere.txt.in',
     ... """
     ... I'm the template that's supposed to replace the file above.
     ... """)
-  
+
     >>> write(sample_buildout, 'buildout.cfg',
     ... """
     ... [buildout]
@@ -137,7 +137,7 @@
     ... """
     ... Hello ${world}!
     ... """)
-  
+
     >>> write(sample_buildout, 'buildout.cfg',
     ... """
     ... [buildout]
@@ -147,12 +147,13 @@
     ... recipe = z3c.recipe.filetemplate
     ... files = missing.txt
     ... """)
-  
-    >>> print system(buildout)
+
+    >>> print system(buildout) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
     Installing missing.
     While:
       Installing missing.
-    Error: Referenced option does not exist: missing world
+    Error: Option 'missing:world', referenced in line 2, col 7 of
+           .../sample-buildout/missing.txt.in, does not exist.
 
 No changes means just an update
 -------------------------------
@@ -365,3 +366,70 @@
     d  parts
     d  template
 
+Specifying files with relative paths at the buildout root
+---------------------------------------------------------
+
+Working at the buildout root follows some different code paths with relative
+paths so we explore those here.  We also evaluate paths at the directory root.
+
+    >>> rmdir(sample_buildout, 'template')
+    >>> mkdir(sample_buildout, 'template')
+    >>> write(sample_buildout, 'template', 'dosomething.py.in', '''\
+    ... #!${buildout:executable}
+    ... ${python-relative-path-setup}
+    ... root = ${buildout:directory|path-repr}
+    ... ''')
+
+    >>> write(sample_buildout, 'template', 'dosomething.sh.in', '''\
+    ... #!/bin/sh
+    ... ${shell-relative-path-setup}
+    ... cat ${buildout:directory|shell-path}
+    ... ''')
+
+    >>> write(sample_buildout, 'buildout.cfg',
+    ... """
+    ... [buildout]
+    ... parts = message
+    ... relative-paths = true
+    ...
+    ... [message]
+    ... recipe = z3c.recipe.filetemplate
+    ... source-directory = template
+    ... """)
+
+    >>> print system(buildout)
+    Uninstalling message.
+    Installing message.
+
+    >>> cat(sample_buildout, 'dosomething.py') # doctest: +ELLIPSIS
+    #!...
+    import os, imp
+    # Get path to this file.
+    if __name__ == '__main__':
+        _z3c_recipe_filetemplate_filename = __file__
+    else:
+        # If this is an imported module, we want the location of the .py
+        # file, not the .pyc, because the .py file may have been symlinked.
+        _z3c_recipe_filetemplate_filename = imp.find_module(__name__)[1]
+    # Get the full, non-symbolic-link directory for this file.
+    _z3c_recipe_filetemplate_base = os.path.dirname(
+        os.path.abspath(os.path.realpath(_z3c_recipe_filetemplate_filename)))
+    # This is the buildout root.
+    def _z3c_recipe_filetemplate_path_repr(path):
+        "Return absolute version of buildout-relative path."
+        return os.path.join(_z3c_recipe_filetemplate_base, path)
+    <BLANKLINE>
+    root = _z3c_recipe_filetemplate_path_repr('.')
+
+    >>> cat(sample_buildout, 'dosomething.sh') # doctest: +ELLIPSIS
+    #!/bin/sh
+    # Get full, non-symbolic-link path to this file.
+    Z3C_RECIPE_FILETEMPLATE_FILENAME=`\
+        readlink -f "$0" 2>/dev/null || \
+        realpath "$0" 2>/dev/null || \
+        type -P "$0" 2>/dev/null`
+    # Get directory of file.
+    Z3C_RECIPE_FILETEMPLATE_BASE=`dirname ${Z3C_RECIPE_FILETEMPLATE_FILENAME}`
+    # This is the buildout root.
+    <BLANKLINE>
+    cat "$Z3C_RECIPE_FILETEMPLATE_BASE"/.



More information about the checkins mailing list