Templating.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. #!/usr/bin/python
  2. #
  3. # Copyright 2011 Nick Mathewson, Michael Stone
  4. #
  5. # You may do anything with this work that copyright law would normally
  6. # restrict, so long as you retain the above notice(s) and this license
  7. # in all redistributed copies and derived works. There is no warranty.
  8. """
  9. This module contins a general-purpose specialization-based
  10. templating mechanism. Chutney uses it for string-substitution to
  11. build torrc files and other such things.
  12. First, there are a few classes to implement "Environments". An
  13. "Environment" is like a dictionary, but delegates keys that it can't
  14. find to a "parent" object, which can be an environment or a dict.
  15. >>> base = Environ(foo=99, bar=600)
  16. >>> derived1 = Environ(parent=base, bar=700, quux=32)
  17. >>> base["foo"]
  18. 99
  19. >>> sorted(base.keys())
  20. ['bar', 'foo']
  21. >>> derived1["foo"]
  22. 99
  23. >>> base["bar"]
  24. 600
  25. >>> derived1["bar"]
  26. 700
  27. >>> derived1["quux"]
  28. 32
  29. >>> sorted(derived1.keys())
  30. ['bar', 'foo', 'quux']
  31. You can declare an environment subclass with methods named
  32. _get_foo() to implement a dictionary whose 'foo' item is
  33. calculated on the fly, and cached.
  34. >>> class Specialized(Environ):
  35. ... def __init__(self, p=None, **kw):
  36. ... Environ.__init__(self, p, **kw)
  37. ... self._n_calls = 0
  38. ... def _get_expensive_value(self, my):
  39. ... self._n_calls += 1
  40. ... return "Let's pretend this is hard to compute"
  41. ...
  42. >>> s = Specialized(base, quux="hi")
  43. >>> s["quux"]
  44. 'hi'
  45. >>> s['expensive_value']
  46. "Let's pretend this is hard to compute"
  47. >>> s['expensive_value']
  48. "Let's pretend this is hard to compute"
  49. >>> s._n_calls
  50. 1
  51. >>> sorted(s.keys())
  52. ['bar', 'expensive_value', 'foo', 'quux']
  53. There's an internal class that extends Python's string.Template
  54. with a slightly more useful syntax for us. (It allows more characters
  55. in its key types.)
  56. >>> bt = _BetterTemplate("Testing ${hello}, $goodbye$$, $foo , ${a:b:c}")
  57. >>> bt.safe_substitute({'a:b:c': "4"}, hello=1, goodbye=2, foo=3)
  58. 'Testing 1, 2$, 3 , 4'
  59. Finally, there's a Template class that implements templates for
  60. simple string substitution. Variables with names like $abc or
  61. ${abc} are replaced by their values; $$ is replaced by $, and
  62. ${include:foo} is replaced by the contents of the file "foo".
  63. >>> t = Template("${include:/dev/null} $hi_there")
  64. >>> sorted(t.freevars())
  65. ['hi_there']
  66. >>> t.format(dict(hi_there=99))
  67. ' 99'
  68. >>> t2 = Template("X$${include:$fname} $bar $baz")
  69. >>> t2.format(dict(fname="/dev/null", bar=33, baz="$foo", foo=1337))
  70. 'X 33 1337'
  71. >>> sorted(t2.freevars({'fname':"/dev/null"}))
  72. ['bar', 'baz', 'fname']
  73. """
  74. from __future__ import with_statement
  75. import string
  76. import os
  77. #class _KeyError(KeyError):
  78. # pass
  79. _KeyError = KeyError
  80. class _DictWrapper(object):
  81. """Base class to implement a dictionary-like object with delegation.
  82. To use it, implement the _getitem method, and pass the optional
  83. 'parent' argument to the constructor.
  84. Lookups are satisfied first by calling _getitem(). If that
  85. fails with KeyError but the parent is present, lookups delegate
  86. to _parent.
  87. """
  88. ## Fields
  89. # _parent: the parent object that lookups delegate to.
  90. def __init__(self, parent=None):
  91. self._parent = parent
  92. def _getitem(self, key):
  93. raise NotImplemented()
  94. def __getitem__(self, key):
  95. try:
  96. return self._getitem(key)
  97. except KeyError:
  98. pass
  99. if self._parent is None:
  100. raise _KeyError(key)
  101. try:
  102. return self._parent[key]
  103. except KeyError:
  104. raise _KeyError(key)
  105. class Environ(_DictWrapper):
  106. """An 'Environ' is used to define a set of key-value mappings with a
  107. fall-back parent Environ. When looking for keys in the
  108. Environ, any keys not found in this Environ are searched for in
  109. the parent.
  110. >>> animal = Environ(mobile=True,legs=4,can_program=False,can_meow=False)
  111. >>> biped = Environ(animal,legs=2)
  112. >>> cat = Environ(animal,can_meow=True)
  113. >>> human = Environ(biped,feathers=False,can_program=True)
  114. >>> chicken = Environ(biped,feathers=True)
  115. >>> human['legs']
  116. 2
  117. >>> human['can_meow']
  118. False
  119. >>> human['can_program']
  120. True
  121. >>> cat['legs']
  122. 4
  123. >>> cat['can_meow']
  124. True
  125. You can extend Environ to support values calculated at run-time by
  126. defining methods with names in the format _get_KEY():
  127. >>> class HomeEnv(Environ):
  128. ... def __init__(self, p=None, **kw):
  129. ... Environ.__init__(self, p, **kw)
  130. ... def _get_homedir(self, my):
  131. ... return os.environ.get("HOME",None)
  132. ... def _get_dotemacs(self, my):
  133. ... return os.path.join(my['homedir'], ".emacs")
  134. >>> h = HomeEnv()
  135. >>> os.path.exists(h["homedir"])
  136. True
  137. >>> h2 = Environ(h, homedir="/tmp")
  138. >>> h2['dotemacs']
  139. '/tmp/.emacs'
  140. Values returned from these _get_KEY functions are cached. The
  141. 'my' argument passed to these functions is the top-level dictionary
  142. that we're using for our lookup.
  143. """
  144. ## Fields
  145. # _dict: dictionary holding the contents of this Environ that are
  146. # not inherited from the parent and are not computed on the fly.
  147. # _cache: dictionary holding already-computed values in this Environ.
  148. def __init__(self, parent=None, **kw):
  149. _DictWrapper.__init__(self, parent)
  150. self._dict = kw
  151. self._cache = {}
  152. def _getitem(self, key):
  153. try:
  154. return self._dict[key]
  155. except KeyError:
  156. pass
  157. try:
  158. return self._cache[key]
  159. except KeyError:
  160. pass
  161. fn = getattr(self, "_get_%s"%key, None)
  162. if fn is not None:
  163. try:
  164. self._cache[key] = rv = fn(self)
  165. return rv
  166. except _KeyError:
  167. raise KeyError(key)
  168. raise KeyError(key)
  169. def __setitem__(self, key, val):
  170. self._dict[key] = val
  171. def keys(self):
  172. s = set()
  173. s.update(self._dict.keys())
  174. s.update(self._cache.keys())
  175. if self._parent is not None:
  176. s.update(self._parent.keys())
  177. s.update(name[5:] for name in dir(self) if name.startswith("_get_"))
  178. return s
  179. class IncluderDict(_DictWrapper):
  180. """Helper to implement ${include:} template substitution. Acts as a
  181. dictionary that maps include:foo to the contents of foo (relative to
  182. a search path if foo is a relative path), and delegates everything else
  183. to a parent.
  184. """
  185. ## Fields
  186. # _includePath: a list of directories to consider when searching
  187. # for files given as relative paths.
  188. # _st_mtime: the most recent time when any of the files included
  189. # so far has been updated. (As seconds since the epoch).
  190. def __init__(self, parent, includePath=(".",)):
  191. """Create a new IncluderDict. Non-include entries are delegated to
  192. parent. Non-absolute paths are searched for relative to the
  193. paths listed in includePath.
  194. """
  195. _DictWrapper.__init__(self, parent)
  196. self._includePath = includePath
  197. self._st_mtime = 0
  198. def _getitem(self, key):
  199. if not key.startswith("include:"):
  200. raise KeyError(key)
  201. filename = key[len("include:"):]
  202. if os.path.isabs(filename):
  203. with open(filename, 'r') as f:
  204. stat = os.fstat(f.fileno())
  205. if stat.st_mtime > self._st_mtime:
  206. self._st_mtime = stat.st_mtime
  207. return f.read()
  208. for elt in self._includePath:
  209. fullname = os.path.join(elt, filename)
  210. if os.path.exists(fullname):
  211. with open(fullname, 'r') as f:
  212. stat = os.fstat(f.fileno())
  213. if stat.st_mtime > self._st_mtime:
  214. self._st_mtime = stat.st_mtime
  215. return f.read()
  216. raise KeyError(key)
  217. def getUpdateTime(self):
  218. return self._st_mtime
  219. class _BetterTemplate(string.Template):
  220. """Subclass of the standard string.Template that allows a wider range of
  221. characters in variable names.
  222. """
  223. idpattern = r'[a-z0-9:_/\.\-]+'
  224. def __init__(self, template):
  225. string.Template.__init__(self, template)
  226. class _FindVarsHelper(object):
  227. """Helper dictionary for finding the free variables in a template.
  228. It answers all requests for key lookups affirmatively, and remembers
  229. what it was asked for.
  230. """
  231. ## Fields
  232. # _dflts: a dictionary of default values to treat specially
  233. # _vars: a set of all the keys that we've been asked to look up so far.
  234. def __init__(self, dflts):
  235. self._dflts = dflts
  236. self._vars = set()
  237. def __getitem__(self, var):
  238. self._vars.add(var)
  239. try:
  240. return self._dflts[var]
  241. except KeyError:
  242. return ""
  243. class Template(object):
  244. """A Template is a string pattern that allows repeated variable
  245. substitutions. These syntaxes are supported:
  246. $var -- expands to the value of var
  247. ${var} -- expands to the value of var
  248. $$ -- expands to a single $
  249. ${include:filename} -- expands to the contents of filename
  250. Substitutions are performed iteratively until no more are possible.
  251. """
  252. # Okay, actually, we stop after this many substitutions to avoid
  253. # infinite loops
  254. MAX_ITERATIONS = 32
  255. ## Fields
  256. # _pat: The base pattern string to start our substitutions from
  257. # _includePath: a list of directories to search when including a file
  258. # by relative path.
  259. def __init__(self, pattern, includePath=(".",)):
  260. self._pat = pattern
  261. self._includePath = includePath
  262. def freevars(self, defaults=None):
  263. """Return a set containing all the free variables in this template"""
  264. if defaults is None:
  265. defaults = {}
  266. d = _FindVarsHelper(defaults)
  267. self.format(d)
  268. return d._vars
  269. def format(self, values):
  270. """Return a string containing this template, filled in with the
  271. values in the mapping 'values'.
  272. """
  273. values = IncluderDict(values, self._includePath)
  274. orig_val = self._pat
  275. nIterations = 0
  276. while True:
  277. v = _BetterTemplate(orig_val).substitute(values)
  278. if v == orig_val:
  279. return v
  280. orig_val = v
  281. nIterations += 1
  282. if nIterations > self.MAX_ITERATIONS:
  283. raise ValueError("Too many iterations in expanding template!")
  284. if __name__ == '__main__':
  285. import sys
  286. if len(sys.argv) == 1:
  287. import doctest
  288. doctest.testmod()
  289. print "done"
  290. else:
  291. for fn in sys.argv[1:]:
  292. with open(fn, 'r') as f:
  293. t = Template(f.read())
  294. print fn, t.freevars()