Browse Source

Add documentation for Templating.py, including a failing test

Nick Mathewson 13 years ago
parent
commit
f116e0bf5b
1 changed files with 129 additions and 0 deletions
  1. 129 0
      lib/chutney/Templating.py

+ 129 - 0
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