aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Mathewson <nickm@torproject.org>2011-03-06 22:22:48 -0500
committerNick Mathewson <nickm@torproject.org>2011-03-06 22:22:52 -0500
commitf116e0bf5b26b93faac2286884680a5dfb89fd3d (patch)
tree955fe2a257673a717c06068f2af4df749def780d
parent7463ccac6e191a958b006fff5f14f51805673f44 (diff)
downloadchutney-f116e0bf5b26b93faac2286884680a5dfb89fd3d.tar
chutney-f116e0bf5b26b93faac2286884680a5dfb89fd3d.tar.gz
Add documentation for Templating.py, including a failing test
-rw-r--r--lib/chutney/Templating.py129
1 files changed, 129 insertions, 0 deletions
diff --git a/lib/chutney/Templating.py b/lib/chutney/Templating.py
index 3000512..eb129c0 100644
--- a/lib/chutney/Templating.py
+++ b/lib/chutney/Templating.py
@@ -7,6 +7,14 @@
# in all redistributed copies and derived works. There is no warranty.
"""
+ This module contins a general-purpose specialization-based
+ templating mechanism. Chutney uses it for string-substitution to
+ build torrc files and other such things.
+
+ First, there are a few classes to implement "Environments". An
+ "Environment" is like a dictionary, but delegates keys that it can't
+ find to a "parent" object, which can be an environment or a dict.
+
>>> base = Environ(foo=99, bar=600)
>>> derived1 = Environ(parent=base, bar=700, quux=32)
>>> base["foo"]
@@ -23,6 +31,11 @@
32
>>> sorted(derived1.keys())
['bar', 'foo', 'quux']
+
+ You can declare an environment subclass with methods named
+ _get_foo() to implement a dictionary whose 'foo' item is
+ calculated on the fly, and cached.
+
>>> class Specialized(Environ):
... def __init__(self, p=None, **kw):
... Environ.__init__(self, p, **kw)
@@ -43,10 +56,19 @@
>>> sorted(s.keys())
['bar', 'expensive_value', 'foo', 'quux']
+ There's an internal class that extends Python's string.Template
+ with a slightly more useful syntax for us. (It allows more characters
+ in its key types.)
+
>>> bt = _BetterTemplate("Testing ${hello}, $goodbye$$, $foo , ${a:b:c}")
>>> bt.safe_substitute({'a:b:c': "4"}, hello=1, goodbye=2, foo=3)
'Testing 1, 2$, 3 , 4'
+ Finally, there's a Template class that implements templates for
+ simple string substitution. Variables with names like $abc or
+ ${abc} are replaced by their values; $$ is replaced by $, and
+ ${include:foo} is replaced by the contents of the file "foo".
+
>>> t = Template("${include:/dev/null} $hi_there")
>>> sorted(t.freevars())
['hi_there']
@@ -71,9 +93,23 @@ import os
_KeyError = KeyError
class _DictWrapper(object):
+ """Base class to implement a dictionary-like object with delegation.
+ To use it, implement the _getitem method, and pass the optional
+ 'parent' argument to the constructor.
+
+ Lookups are satisfied first by calling _getitem(). If that
+ fails with KeyError but the parent is present, lookups delegate
+ to _parent.
+ """
+ ## Fields
+ # _parent: the parent object that lookups delegate to.
+
def __init__(self, parent=None):
self._parent = parent
+ def _getitem(self, key):
+ raise NotImplemented()
+
def __getitem__(self, key):
try:
return self._getitem(key)
@@ -88,6 +124,53 @@ class _DictWrapper(object):
raise _KeyError(key)
class Environ(_DictWrapper):
+ """An 'Environ' is used to define a set of key-value mappings with a
+ fall-back parent Environ. When looking for keys in the
+ Environ, any keys not found in this Environ are searched for in
+ the parent.
+
+ >>> animal = Environ(mobile=True,legs=4,can_program=False,can_meow=False)
+ >>> biped = Environ(animal,legs=2)
+ >>> cat = Environ(animal,can_meow=True)
+ >>> human = Environ(biped,feathers=False,can_program=True)
+ >>> chicken = Environ(biped,feathers=True)
+ >>> human['legs']
+ 2
+ >>> human['can_meow']
+ False
+ >>> human['can_program']
+ True
+ >>> cat['legs']
+ 4
+ >>> cat['can_meow']
+ True
+
+ You can extend Environ to support values calculated at run-time by
+ defining methods with names in the format _get_KEY():
+
+ >>> class HomeEnv(Environ):
+ ... def __init__(self, p=None, **kw):
+ ... Environ.__init__(self, p, **kw)
+ ... def _get_homedir(self, my):
+ ... return os.environ.get("HOME",None)
+ ... def _get_dotemacs(self, my):
+ ... return os.path.join(my['homedir'], ".emacs")
+ >>> h = HomeEnv()
+ >>> os.path.exists(h["homedir"])
+ True
+ >>> h2 = Environ(h, homedir="/tmp")
+ >>> h2['dotemacs']
+ '/tmp/.emacs'
+
+ Values returned from these _get_KEY functions are cached. The
+ 'my' argument passed to these functions is the top-level dictionary
+ that we're using for our lookup.
+ """
+ ## Fields
+ # _dict: dictionary holding the contents of this Environ that are
+ # not inherited from the parent and are not computed on the fly.
+ # _cache: dictionary holding already-computed values in this Environ.
+
def __init__(self, parent=None, **kw):
_DictWrapper.__init__(self, parent)
self._dict = kw
@@ -124,7 +207,22 @@ class Environ(_DictWrapper):
return s
class IncluderDict(_DictWrapper):
+ """Helper to implement ${include:} template substitution. Acts as a
+ dictionary that maps include:foo to the contents of foo (relative to
+ a search path if foo is a relative path), and delegates everything else
+ to a parent.
+ """
+ ## Fields
+ # _includePath: a list of directories to consider when searching
+ # for files given as relative paths.
+ # _st_mtime: the most recent time when any of the files included
+ # so far has been updated. (As seconds since the epoch).
+
def __init__(self, parent, includePath=(".",)):
+ """Create a new IncluderDict. Non-include entries are delegated to
+ parent. Non-absolute paths are searched for relative to the
+ paths listed in includePath.
+ """
_DictWrapper.__init__(self, parent)
self._includePath = includePath
self._st_mtime = 0
@@ -156,6 +254,9 @@ class IncluderDict(_DictWrapper):
return self._st_mtime
class _BetterTemplate(string.Template):
+ """Subclass of the standard string.Template that allows a wider range of
+ characters in variable names.
+ """
idpattern = r'[a-z0-9:_/\.\-]+'
@@ -163,6 +264,13 @@ class _BetterTemplate(string.Template):
string.Template.__init__(self, template)
class _FindVarsHelper(object):
+ """Helper dictionary for finding the free variables in a template.
+ It answers all requests for key lookups affirmatively, and remembers
+ what it was asked for.
+ """
+ ## Fields
+ # _dflts: a dictionary of default values to treat specially
+ # _vars: a set of all the keys that we've been asked to look up so far.
def __init__(self, dflts):
self._dflts = dflts
self._vars = set()
@@ -174,13 +282,31 @@ class _FindVarsHelper(object):
return ""
class Template(object):
+ """A Template is a string pattern that allows repeated variable
+ substitutions. These syntaxes are supported:
+ $var -- expands to the value of var
+ ${var} -- expands to the value of var
+ $$ -- expands to a single $
+ ${include:filename} -- expands to the contents of filename
+
+ Substitutions are performed iteratively until no more are possible.
+ """
+
+ # Okay, actually, we stop after this many substitutions to avoid
+ # infinite loops
MAX_ITERATIONS = 32
+ ## Fields
+ # _pat: The base pattern string to start our substitutions from
+ # _includePath: a list of directories to search when including a file
+ # by relative path.
+
def __init__(self, pattern, includePath=(".",)):
self._pat = pattern
self._includePath = includePath
def freevars(self, defaults=None):
+ """Return a set containing all the free variables in this template"""
if defaults is None:
defaults = {}
d = _FindVarsHelper(defaults)
@@ -188,6 +314,9 @@ class Template(object):
return d._vars
def format(self, values):
+ """Return a string containing this template, filled in with the
+ values in the mapping 'values'.
+ """
values = IncluderDict(values, self._includePath)
orig_val = self._pat
nIterations = 0