PageRenderTime 52ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/pyccuracy/airspeed.py

http://github.com/heynemann/pyccuracy
Python | 957 lines | 942 code | 10 blank | 5 comment | 10 complexity | 5646d486ac04f825276be3136895c60a MD5 | raw file
Possible License(s): CC-BY-SA-3.0
  1. #!/usr/bin/env python
  2. # From http://dev.sanityinc.com/airspeed/wiki
  3. import re, operator, os
  4. import StringIO # cStringIO has issues with unicode
  5. __all__ = ['Template', 'TemplateError', 'TemplateSyntaxError', 'CachingFileLoader']
  6. ###############################################################################
  7. # Compatibility for old Pythons & Jython
  8. ###############################################################################
  9. try: True
  10. except NameError:
  11. False, True = 0, 1
  12. try: dict
  13. except NameError:
  14. from UserDict import UserDict
  15. class dict(UserDict):
  16. def __init__(self): self.data = {}
  17. try: operator.__gt__
  18. except AttributeError:
  19. operator.__gt__ = lambda a, b: a > b
  20. operator.__lt__ = lambda a, b: a < b
  21. operator.__ge__ = lambda a, b: a >= b
  22. operator.__le__ = lambda a, b: a <= b
  23. operator.__eq__ = lambda a, b: a == b
  24. operator.__ne__ = lambda a, b: a != b
  25. operator.mod = lambda a, b: a % b
  26. try:
  27. basestring
  28. def is_string(s): return isinstance(s, basestring)
  29. except NameError:
  30. def is_string(s): return type(s) == type('')
  31. ###############################################################################
  32. # Public interface
  33. ###############################################################################
  34. def boolean_value(variable_value):
  35. if variable_value == False: return False
  36. return not (variable_value is None)
  37. class Template:
  38. def __init__(self, content):
  39. self.content = content
  40. self.root_element = None
  41. def merge(self, namespace, loader=None):
  42. output = StoppableStream()
  43. self.merge_to(namespace, output, loader)
  44. return output.getvalue()
  45. def ensure_compiled(self):
  46. if not self.root_element:
  47. self.root_element = TemplateBody(self.content)
  48. def merge_to(self, namespace, fileobj, loader=None):
  49. if loader is None: loader = NullLoader()
  50. self.ensure_compiled()
  51. self.root_element.evaluate(fileobj, namespace, loader)
  52. class TemplateError(Exception):
  53. pass
  54. class TemplateSyntaxError(TemplateError):
  55. def __init__(self, element, expected):
  56. self.element = element
  57. self.text_understood = element.full_text()[:element.end]
  58. self.line = 1 + self.text_understood.count('\n')
  59. self.column = len(self.text_understood) - self.text_understood.rfind('\n')
  60. got = element.next_text()
  61. if len(got) > 40:
  62. got = got[:36] + ' ...'
  63. Exception.__init__(self, "line %d, column %d: expected %s in %s, got: %s ..." % (self.line, self.column, expected, self.element_name(), got))
  64. def get_position_strings(self):
  65. error_line_start = 1 + self.text_understood.rfind('\n')
  66. if '\n' in self.element.next_text():
  67. error_line_end = self.element.next_text().find('\n') + self.element.end
  68. else:
  69. error_line_end = len(self.element.full_text())
  70. error_line = self.element.full_text()[error_line_start:error_line_end]
  71. caret_pos = self.column
  72. return [error_line, ' ' * (caret_pos - 1) + '^']
  73. def element_name(self):
  74. return re.sub('([A-Z])', lambda m: ' ' + m.group(1).lower(), self.element.__class__.__name__).strip()
  75. class NullLoader:
  76. def load_text(self, name):
  77. raise TemplateError("no loader available for '%s'" % name)
  78. def load_template(self, name):
  79. raise self.load_text(name)
  80. class CachingFileLoader:
  81. def __init__(self, basedir, debugging=False):
  82. self.basedir = basedir
  83. self.known_templates = {} # name -> (template, file_mod_time)
  84. self.debugging = debugging
  85. if debugging: print "creating caching file loader with basedir:", basedir
  86. def filename_of(self, name):
  87. return os.path.join(self.basedir, name)
  88. def load_text(self, name):
  89. if self.debugging: print "Loading text from", self.basedir, name
  90. f = open(self.filename_of(name))
  91. try: return f.read()
  92. finally: f.close()
  93. def load_template(self, name):
  94. if self.debugging: print "Loading template...", name,
  95. mtime = os.path.getmtime(self.filename_of(name))
  96. if self.known_templates.has_key(name):
  97. template, prev_mtime = self.known_templates[name]
  98. if mtime <= prev_mtime:
  99. if self.debugging: print "loading parsed template from cache"
  100. return template
  101. if self.debugging: print "loading text from disk"
  102. template = Template(self.load_text(name))
  103. template.ensure_compiled()
  104. self.known_templates[name] = (template, mtime)
  105. return template
  106. class StoppableStream(StringIO.StringIO):
  107. def __init__(self, buf=''):
  108. self.stop = False
  109. StringIO.StringIO.__init__(self, buf)
  110. def write(self, s):
  111. if not self.stop:
  112. StringIO.StringIO.write(self, s)
  113. ###############################################################################
  114. # Internals
  115. ###############################################################################
  116. WHITESPACE_TO_END_OF_LINE = re.compile(r'[ \t\r]*\n(.*)', re.S)
  117. class NoMatch(Exception): pass
  118. class LocalNamespace(dict):
  119. def __init__(self, parent):
  120. dict.__init__(self)
  121. self.parent = parent
  122. def __getitem__(self, key):
  123. try: return dict.__getitem__(self, key)
  124. except KeyError:
  125. parent_value = self.parent[key]
  126. self[key] = parent_value
  127. return parent_value
  128. def top(self):
  129. if hasattr(self.parent, "top"):
  130. return self.parent.top()
  131. return self.parent
  132. def __repr__(self):
  133. return dict.__repr__(self) + '->' + repr(self.parent)
  134. class _Element:
  135. def __init__(self, text, start=0):
  136. self._full_text = text
  137. self.start = self.end = start
  138. self.parse()
  139. def next_text(self):
  140. return self._full_text[self.end:]
  141. def my_text(self):
  142. return self._full_text[self.start:self.end]
  143. def full_text(self):
  144. return self._full_text
  145. def syntax_error(self, expected):
  146. return TemplateSyntaxError(self, expected)
  147. def identity_match(self, pattern):
  148. m = pattern.match(self._full_text, self.end)
  149. if not m: raise NoMatch()
  150. self.end = m.start(pattern.groups)
  151. return m.groups()[:-1]
  152. def next_match(self, pattern):
  153. m = pattern.match(self._full_text, self.end)
  154. if not m: return False
  155. self.end = m.start(pattern.groups)
  156. return m.groups()[:-1]
  157. def optional_match(self, pattern):
  158. m = pattern.match(self._full_text, self.end)
  159. if not m: return False
  160. self.end = m.start(pattern.groups)
  161. return True
  162. def require_match(self, pattern, expected):
  163. m = pattern.match(self._full_text, self.end)
  164. if not m: raise self.syntax_error(expected)
  165. self.end = m.start(pattern.groups)
  166. return m.groups()[:-1]
  167. def next_element(self, element_spec):
  168. if callable(element_spec):
  169. element = element_spec(self._full_text, self.end)
  170. self.end = element.end
  171. return element
  172. else:
  173. for element_class in element_spec:
  174. try: element = element_class(self._full_text, self.end)
  175. except NoMatch: pass
  176. else:
  177. self.end = element.end
  178. return element
  179. raise NoMatch()
  180. def require_next_element(self, element_spec, expected):
  181. if callable(element_spec):
  182. try: element = element_spec(self._full_text, self.end)
  183. except NoMatch: raise self.syntax_error(expected)
  184. else:
  185. self.end = element.end
  186. return element
  187. else:
  188. for element_class in element_spec:
  189. try: element = element_class(self._full_text, self.end)
  190. except NoMatch: pass
  191. else:
  192. self.end = element.end
  193. return element
  194. expected = ', '.join([cls.__name__ for cls in element_spec])
  195. raise self.syntax_error('one of: ' + expected)
  196. class Text(_Element):
  197. PLAIN = re.compile(r'((?:[^\\\$#]+|\\[\$#])+|\$[^!\{a-z0-9_]|\$$|#$|#[^\{\}a-zA-Z0-9#\*]+|\\.)(.*)$', re.S + re.I)
  198. ESCAPED_CHAR = re.compile(r'\\([\\\$#])')
  199. def parse(self):
  200. text, = self.identity_match(self.PLAIN)
  201. def unescape(match):
  202. return match.group(1)
  203. self.text = self.ESCAPED_CHAR.sub(unescape, text)
  204. def evaluate(self, stream, namespace, loader):
  205. stream.write(self.text)
  206. class FallthroughHashText(_Element):
  207. """ Plain tex, starting a hash, but which wouldn't be matched
  208. by a directive or a macro earlier.
  209. The canonical example is an HTML color spec.
  210. Another good example, is in-document hypertext links
  211. (or the dummy versions thereof often used a href targets
  212. when javascript is used.
  213. Note that it MUST NOT match block-ending directives. """
  214. # because of earlier elements, this will always start with a hash
  215. PLAIN = re.compile(r'(\#+\{?[\d\w]*\}?)(.*)$', re.S)
  216. def parse(self):
  217. self.text, = self.identity_match(self.PLAIN)
  218. if self.text.startswith('#end') or self.text.startswith('#{end}') or self.text.startswith('#else') or self.text.startswith('#{else}') or self.text.startswith('#elseif') or self.text.startswith('#{elseif}'):
  219. raise NoMatch
  220. def evaluate(self, stream, namespace, loader):
  221. stream.write(self.text)
  222. class IntegerLiteral(_Element):
  223. INTEGER = re.compile(r'(-?\d+)(.*)', re.S)
  224. def parse(self):
  225. self.value, = self.identity_match(self.INTEGER)
  226. self.value = int(self.value)
  227. def calculate(self, namespace, loader):
  228. return self.value
  229. class FloatingPointLiteral(_Element):
  230. FLOAT = re.compile(r'(-?\d+\.\d+)(.*)', re.S)
  231. def parse(self):
  232. self.value, = self.identity_match(self.FLOAT)
  233. self.value = float(self.value)
  234. def calculate(self, namespace, loader):
  235. return self.value
  236. class BooleanLiteral(_Element):
  237. BOOLEAN = re.compile(r'((?:true)|(?:false))(.*)', re.S | re.I)
  238. def parse(self):
  239. self.value, = self.identity_match(self.BOOLEAN)
  240. self.value = self.value.lower() == 'true'
  241. def calculate(self, namespace, loader):
  242. return self.value
  243. class StringLiteral(_Element):
  244. STRING = re.compile(r"'((?:\\['nrbt\\\\\\$]|[^'\\])*)'(.*)", re.S)
  245. ESCAPED_CHAR = re.compile(r"\\([nrbt'\\])")
  246. def parse(self):
  247. value, = self.identity_match(self.STRING)
  248. def unescape(match):
  249. return {'n': '\n', 'r': '\r', 'b': '\b', 't': '\t', '"': '"', '\\': '\\', "'": "'"}.get(match.group(1), '\\' + match.group(1))
  250. self.value = self.ESCAPED_CHAR.sub(unescape, value)
  251. def calculate(self, namespace, loader):
  252. return self.value
  253. class InterpolatedStringLiteral(StringLiteral):
  254. STRING = re.compile(r'"((?:\\["nrbt\\\\\\$]|[^"\\])*)"(.*)', re.S)
  255. ESCAPED_CHAR = re.compile(r'\\([nrbt"\\])')
  256. def parse(self):
  257. StringLiteral.parse(self)
  258. self.block = Block(self.value, 0)
  259. def calculate(self, namespace, loader):
  260. output = StoppableStream()
  261. self.block.evaluate(output, namespace, loader)
  262. return output.getvalue()
  263. class Range(_Element):
  264. MIDDLE = re.compile(r'([ \t]*\.\.[ \t]*)(.*)$', re.S)
  265. def parse(self):
  266. self.value1 = self.next_element((FormalReference, IntegerLiteral))
  267. self.identity_match(self.MIDDLE)
  268. self.value2 = self.next_element((FormalReference, IntegerLiteral))
  269. def calculate(self, namespace, loader):
  270. value1 = self.value1.calculate(namespace, loader)
  271. value2 = self.value2.calculate(namespace, loader)
  272. if value2 < value1:
  273. return xrange(value1, value2 - 1, -1)
  274. return xrange(value1, value2 + 1)
  275. class ValueList(_Element):
  276. COMMA = re.compile(r'\s*,\s*(.*)$', re.S)
  277. def parse(self):
  278. self.values = []
  279. try: value = self.next_element(Value)
  280. except NoMatch:
  281. pass
  282. else:
  283. self.values.append(value)
  284. while self.optional_match(self.COMMA):
  285. value = self.require_next_element(Value, 'value')
  286. self.values.append(value)
  287. def calculate(self, namespace, loader):
  288. return [value.calculate(namespace, loader) for value in self.values]
  289. class _EmptyValues:
  290. def calculate(self, namespace, loader):
  291. return []
  292. class ArrayLiteral(_Element):
  293. START = re.compile(r'\[[ \t]*(.*)$', re.S)
  294. END = re.compile(r'[ \t]*\](.*)$', re.S)
  295. values = _EmptyValues()
  296. def parse(self):
  297. self.identity_match(self.START)
  298. try:
  299. self.values = self.next_element((Range, ValueList))
  300. except NoMatch:
  301. pass
  302. self.require_match(self.END, ']')
  303. self.calculate = self.values.calculate
  304. class DictionaryLiteral(_Element):
  305. START = re.compile(r'{[ \t]*(.*)$', re.S)
  306. END = re.compile(r'[ \t]*}(.*)$', re.S)
  307. KEYVALSEP = re.compile(r'[ \t]*:[[ \t]*(.*)$', re.S)
  308. PAIRSEP = re.compile(r'[ \t]*,[ \t]*(.*)$', re.S)
  309. def parse(self):
  310. self.identity_match(self.START)
  311. self.local_data = {}
  312. if self.optional_match(self.END):
  313. # it's an empty dictionary
  314. return
  315. while(True):
  316. key = self.next_element(Value)
  317. self.require_match(self.KEYVALSEP, ':')
  318. value = self.next_element(Value)
  319. self.local_data[key] = value
  320. if not self.optional_match(self.PAIRSEP): break
  321. self.require_match(self.END, '}')
  322. # Note that this delays calculation of values until it's used.
  323. # TODO confirm that that's correct.
  324. def calculate(self, namespace, loader):
  325. tmp = {}
  326. for (key,val) in self.local_data.items():
  327. tmp[key.calculate(namespace, loader)] = val.calculate(namespace, loader)
  328. return tmp
  329. class Value(_Element):
  330. def parse(self):
  331. self.expression = self.next_element((FormalReference, FloatingPointLiteral, IntegerLiteral,
  332. StringLiteral, InterpolatedStringLiteral, ArrayLiteral,
  333. DictionaryLiteral, ParenthesizedExpression, UnaryOperatorValue,
  334. BooleanLiteral))
  335. def calculate(self, namespace, loader):
  336. return self.expression.calculate(namespace, loader)
  337. class NameOrCall(_Element):
  338. NAME = re.compile(r'([a-zA-Z_][a-zA-Z0-9_]*)(.*)$', re.S)
  339. parameters = None
  340. def parse(self):
  341. self.name, = self.identity_match(self.NAME)
  342. try: self.parameters = self.next_element(ParameterList)
  343. except NoMatch: pass
  344. def calculate(self, current_object, loader, top_namespace):
  345. look_in_dict = True
  346. if not isinstance(current_object, LocalNamespace):
  347. try:
  348. result = getattr(current_object, self.name)
  349. look_in_dict = False
  350. except AttributeError:
  351. pass
  352. if look_in_dict:
  353. try: result = current_object[self.name]
  354. except KeyError: result = None
  355. except TypeError: result = None
  356. except AttributeError: result = None
  357. if result is None:
  358. return None ## TODO: an explicit 'not found' exception?
  359. if self.parameters is not None:
  360. result = result(*self.parameters.calculate(top_namespace, loader))
  361. return result
  362. class SubExpression(_Element):
  363. DOT = re.compile('\.(.*)', re.S)
  364. def parse(self):
  365. self.identity_match(self.DOT)
  366. self.expression = self.next_element(VariableExpression)
  367. def calculate(self, current_object, loader, global_namespace):
  368. return self.expression.calculate(current_object, loader, global_namespace)
  369. class VariableExpression(_Element):
  370. subexpression = None
  371. def parse(self):
  372. self.part = self.next_element(NameOrCall)
  373. try: self.subexpression = self.next_element(SubExpression)
  374. except NoMatch: pass
  375. def calculate(self, namespace, loader, global_namespace=None):
  376. if global_namespace is None:
  377. global_namespace = namespace
  378. value = self.part.calculate(namespace, loader, global_namespace)
  379. if self.subexpression:
  380. value = self.subexpression.calculate(value, loader, global_namespace)
  381. return value
  382. class ParameterList(_Element):
  383. START = re.compile(r'\(\s*(.*)$', re.S)
  384. COMMA = re.compile(r'\s*,\s*(.*)$', re.S)
  385. END = re.compile(r'\s*\)(.*)$', re.S)
  386. values = _EmptyValues()
  387. def parse(self):
  388. self.identity_match(self.START)
  389. try: self.values = self.next_element(ValueList)
  390. except NoMatch: pass
  391. self.require_match(self.END, ')')
  392. def calculate(self, namespace, loader):
  393. return self.values.calculate(namespace, loader)
  394. class FormalReference(_Element):
  395. START = re.compile(r'\$(!?)(\{?)(.*)$', re.S)
  396. CLOSING_BRACE = re.compile(r'\}(.*)$', re.S)
  397. def parse(self):
  398. self.silent, braces = self.identity_match(self.START)
  399. self.expression = self.require_next_element(VariableExpression, 'expression')
  400. if braces: self.require_match(self.CLOSING_BRACE, '}')
  401. self.calculate = self.expression.calculate
  402. def evaluate(self, stream, namespace, loader):
  403. value = self.expression.calculate(namespace, loader)
  404. if value is None:
  405. if self.silent: value = ''
  406. else: value = self.my_text()
  407. if is_string(value):
  408. stream.write(value)
  409. elif isinstance(value, Exception):
  410. stream.write(unicode(value))
  411. else:
  412. stream.write(str(value))
  413. class Null:
  414. def evaluate(self, stream, namespace, loader): pass
  415. class Comment(_Element, Null):
  416. COMMENT = re.compile('#(?:#.*?(?:\n|$)|\*.*?\*#(?:[ \t]*\n)?)(.*)$', re.M + re.S)
  417. def parse(self):
  418. self.identity_match(self.COMMENT)
  419. class BinaryOperator(_Element):
  420. BINARY_OP = re.compile(r'\s*(>=|<=|<|==|!=|>|%|\|\||&&|or|and|\+|\-|\*|\/|\%)\s*(.*)$', re.S)
  421. OPERATORS = {'>' : operator.gt, '>=': operator.ge,
  422. '<' : operator.lt, '<=': operator.le,
  423. '==': operator.eq, '!=': operator.ne,
  424. '%' : operator.mod,
  425. '||': lambda a,b : boolean_value(a) or boolean_value(b),
  426. '&&': lambda a,b : boolean_value(a) and boolean_value(b),
  427. 'or': lambda a,b : boolean_value(a) or boolean_value(b),
  428. 'and': lambda a,b : boolean_value(a) and boolean_value(b),
  429. '+' : operator.add,
  430. '-' : operator.sub,
  431. '*' : operator.mul,
  432. '/' : operator.div}
  433. PRECEDENCE = { '>' : 2, '<' : 2, '==': 2, '>=' : 2, '<=' : 2, '!=': 2,
  434. '||' : 1, '&&' : 1, 'or': 1, 'and': 1,
  435. '+' : 3, '-' : 3, '*' : 3, '/' : 3, '%': 3}
  436. # In velocity, if + is applied to one string and one numeric
  437. # argument, will convert the number into a string.
  438. # As far as I can tell, this is undocumented.
  439. # Note that this applies only to add, not to other operators
  440. def parse(self):
  441. op_string, = self.identity_match(self.BINARY_OP)
  442. self.apply_to = self.OPERATORS[op_string]
  443. self.precedence = self.PRECEDENCE[op_string]
  444. # This assumes that the self operator is "to the left"
  445. # of the argument, and thus gets higher precedence if they're
  446. # both boolean operators.
  447. # That is, the way this is used (see Expression.calculate)
  448. # it should return false if the two ops have the same precedence
  449. # that is, it's strictly greater than, not greater than or equal to
  450. # to get proper left-to-right evaluation, it should skew towards false.
  451. def greater_precedence_than(self, other):
  452. return self.precedence > other.precedence
  453. class UnaryOperatorValue(_Element):
  454. UNARY_OP = re.compile(r'\s*(!)\s*(.*)$', re.S)
  455. OPERATORS = {'!': operator.__not__}
  456. def parse(self):
  457. op_string, = self.identity_match(self.UNARY_OP)
  458. self.value = self.next_element(Value)
  459. self.op = self.OPERATORS[op_string]
  460. def calculate(self, namespace, loader):
  461. return self.op(self.value.calculate(namespace, loader))
  462. # Note: there appears to be no way to differentiate a variable or
  463. # value from an expression, other than context.
  464. class Expression(_Element):
  465. def parse(self):
  466. self.expression = [self.next_element(Value)]
  467. while(True):
  468. try:
  469. binary_operator = self.next_element(BinaryOperator)
  470. value = self.require_next_element(Value, 'value')
  471. self.expression.append(binary_operator)
  472. self.expression.append(value)
  473. except NoMatch:
  474. break
  475. def calculate(self, namespace, loader):
  476. if not self.expression or len(self.expression) == 0:
  477. return False
  478. #TODO: how does velocity deal with an empty condition expression?
  479. opstack = []
  480. valuestack = [self.expression[0]]
  481. terms = self.expression[1:]
  482. # use top of opstack on top 2 values of valuestack
  483. def stack_calculate(ops, values, namespace, loader):
  484. value2 = values.pop()
  485. if isinstance(value2, Value):
  486. value2 = value2.calculate(namespace, loader)
  487. value1 = values.pop()
  488. if isinstance(value1, Value):
  489. value1 = value1.calculate(namespace, loader)
  490. result = ops.pop().apply_to(value1, value2)
  491. # TODO this doesn't short circuit -- does velocity?
  492. # also note they're eval'd out of order
  493. values.append(result)
  494. while terms:
  495. # next is a binary operator
  496. if not opstack or terms[0].greater_precedence_than(opstack[-1]):
  497. opstack.append(terms[0])
  498. valuestack.append(terms[1])
  499. terms = terms[2:]
  500. else:
  501. stack_calculate(opstack, valuestack, namespace, loader)
  502. # now clean out the stacks
  503. while opstack:
  504. stack_calculate(opstack, valuestack, namespace, loader)
  505. if len(valuestack) != 1:
  506. print "evaluation of expression in Condition.calculate is messed up: final length of stack is not one"
  507. #TODO handle this officially
  508. result = valuestack[0]
  509. if isinstance(result, Value):
  510. result = result.calculate(namespace, loader)
  511. return result
  512. class ParenthesizedExpression(_Element):
  513. START = re.compile(r'\(\s*(.*)$', re.S)
  514. END = re.compile(r'\s*\)(.*)$', re.S)
  515. def parse(self):
  516. self.identity_match(self.START)
  517. expression = self.next_element(Expression)
  518. self.require_match(self.END, ')')
  519. self.calculate = expression.calculate
  520. class Condition(_Element):
  521. def parse(self):
  522. expression = self.next_element(ParenthesizedExpression)
  523. self.optional_match(WHITESPACE_TO_END_OF_LINE)
  524. self.calculate = expression.calculate
  525. # TODO do I need to do anything else here?
  526. class End(_Element):
  527. END = re.compile(r'#(?:end|{end})(.*)', re.I + re.S)
  528. def parse(self):
  529. self.identity_match(self.END)
  530. self.optional_match(WHITESPACE_TO_END_OF_LINE)
  531. class ElseBlock(_Element):
  532. START = re.compile(r'#(?:else|{else})(.*)$', re.S + re.I)
  533. def parse(self):
  534. self.identity_match(self.START)
  535. self.block = self.require_next_element(Block, 'block')
  536. self.evaluate = self.block.evaluate
  537. class ElseifBlock(_Element):
  538. START = re.compile(r'#elseif\b\s*(.*)$', re.S + re.I)
  539. def parse(self):
  540. self.identity_match(self.START)
  541. self.condition = self.require_next_element(Condition, 'condition')
  542. self.block = self.require_next_element(Block, 'block')
  543. self.calculate = self.condition.calculate
  544. self.evaluate = self.block.evaluate
  545. class IfDirective(_Element):
  546. START = re.compile(r'#if\b\s*(.*)$', re.S + re.I)
  547. else_block = Null()
  548. def parse(self):
  549. self.identity_match(self.START)
  550. self.condition = self.next_element(Condition)
  551. self.block = self.require_next_element(Block, "block")
  552. self.elseifs = []
  553. while True:
  554. try: self.elseifs.append(self.next_element(ElseifBlock))
  555. except NoMatch: break
  556. try: self.else_block = self.next_element(ElseBlock)
  557. except NoMatch: pass
  558. self.require_next_element(End, '#else, #elseif or #end')
  559. def evaluate(self, stream, namespace, loader):
  560. if self.condition.calculate(namespace, loader):
  561. self.block.evaluate(stream, namespace, loader)
  562. else:
  563. for elseif in self.elseifs:
  564. if elseif.calculate(namespace, loader):
  565. elseif.evaluate(stream, namespace, loader)
  566. return
  567. self.else_block.evaluate(stream, namespace, loader)
  568. # This can't deal with assignments like
  569. # #set($one.two().three = something)
  570. # yet
  571. class Assignment(_Element):
  572. START = re.compile(r'\s*\(\s*\$([a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*)\s*=\s*(.*)$', re.S + re.I)
  573. END = re.compile(r'\s*\)(?:[ \t]*\r?\n)?(.*)$', re.S + re.M)
  574. def parse(self):
  575. var_name, = self.identity_match(self.START)
  576. self.terms = var_name.split('.')
  577. self.value = self.require_next_element(Expression, "expression")
  578. self.require_match(self.END, ')')
  579. def evaluate(self, stream, namespace, loader):
  580. thingy = namespace
  581. for term in self.terms[0:-1]:
  582. if thingy == None: return
  583. look_in_dict = True
  584. if not isinstance(thingy, LocalNamespace):
  585. try:
  586. thingy = getattr(thingy, term)
  587. look_in_dict = False
  588. except AttributeError:
  589. pass
  590. if look_in_dict:
  591. try:
  592. thingy = thingy[term]
  593. except KeyError: thingy = None
  594. except TypeError: thingy = None
  595. except AttributeError: thingy = None
  596. if thingy is not None:
  597. thingy[self.terms[-1]] = self.value.calculate(namespace, loader)
  598. class MacroDefinition(_Element):
  599. START = re.compile(r'#macro\b(.*)', re.S + re.I)
  600. OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S)
  601. NAME = re.compile(r'\s*([a-z][a-z_0-9]*)\b(.*)', re.S + re.I)
  602. CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S)
  603. ARG_NAME = re.compile(r'[, \t]+\$([a-z][a-z_0-9]*)(.*)$', re.S + re.I)
  604. RESERVED_NAMES = ('if', 'else', 'elseif', 'set', 'macro', 'foreach', 'parse', 'include', 'stop', 'end')
  605. def parse(self):
  606. self.identity_match(self.START)
  607. self.require_match(self.OPEN_PAREN, '(')
  608. self.macro_name, = self.require_match(self.NAME, 'macro name')
  609. if self.macro_name.lower() in self.RESERVED_NAMES:
  610. raise self.syntax_error('non-reserved name')
  611. self.arg_names = []
  612. while True:
  613. m = self.next_match(self.ARG_NAME)
  614. if not m: break
  615. self.arg_names.append(m[0])
  616. self.require_match(self.CLOSE_PAREN, ') or arg name')
  617. self.optional_match(WHITESPACE_TO_END_OF_LINE)
  618. self.block = self.require_next_element(Block, 'block')
  619. self.require_next_element(End, 'block')
  620. def evaluate(self, stream, namespace, loader):
  621. global_ns = namespace.top()
  622. macro_key = '#' + self.macro_name.lower()
  623. if global_ns.has_key(macro_key):
  624. raise Exception("cannot redefine macro")
  625. global_ns[macro_key] = self
  626. def execute_macro(self, stream, namespace, arg_value_elements, loader):
  627. if len(arg_value_elements) != len(self.arg_names):
  628. raise Exception("expected %d arguments, got %d" % (len(self.arg_names), len(arg_value_elements)))
  629. macro_namespace = LocalNamespace(namespace)
  630. for arg_name, arg_value in zip(self.arg_names, arg_value_elements):
  631. macro_namespace[arg_name] = arg_value.calculate(namespace, loader)
  632. self.block.evaluate(stream, macro_namespace, loader)
  633. class MacroCall(_Element):
  634. START = re.compile(r'#([a-z][a-z_0-9]*)\b(.*)', re.S + re.I)
  635. OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S)
  636. CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S)
  637. SPACE_OR_COMMA = re.compile(r'[ \t]*(?:,|[ \t])[ \t]*(.*)$', re.S)
  638. def parse(self):
  639. macro_name, = self.identity_match(self.START)
  640. self.macro_name = macro_name.lower()
  641. self.args = []
  642. if self.macro_name in MacroDefinition.RESERVED_NAMES or self.macro_name.startswith('end'):
  643. raise NoMatch()
  644. if not self.optional_match(self.OPEN_PAREN):
  645. # It's not really a macro call,
  646. # it's just a spare pound sign with text after it,
  647. # the typical example being a color spec: "#ffffff"
  648. # call it not-a-match and then let another thing catch it
  649. raise NoMatch()
  650. while True:
  651. try: self.args.append(self.next_element(Value))
  652. except NoMatch: break
  653. if not self.optional_match(self.SPACE_OR_COMMA): break
  654. self.require_match(self.CLOSE_PAREN, 'argument value or )')
  655. def evaluate(self, stream, namespace, loader):
  656. try: macro = namespace['#' + self.macro_name]
  657. except KeyError: raise Exception('no such macro: ' + self.macro_name)
  658. macro.execute_macro(stream, namespace, self.args, loader)
  659. class IncludeDirective(_Element):
  660. START = re.compile(r'#include\b(.*)', re.S + re.I)
  661. OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S)
  662. CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S)
  663. def parse(self):
  664. self.identity_match(self.START)
  665. self.require_match(self.OPEN_PAREN, '(')
  666. self.name = self.require_next_element((StringLiteral, InterpolatedStringLiteral, FormalReference), 'template name')
  667. self.require_match(self.CLOSE_PAREN, ')')
  668. def evaluate(self, stream, namespace, loader):
  669. stream.write(loader.load_text(self.name.calculate(namespace, loader)))
  670. class ParseDirective(_Element):
  671. START = re.compile(r'#parse\b(.*)', re.S + re.I)
  672. OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S)
  673. CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S)
  674. def parse(self):
  675. self.identity_match(self.START)
  676. self.require_match(self.OPEN_PAREN, '(')
  677. self.name = self.require_next_element((StringLiteral, InterpolatedStringLiteral, FormalReference), 'template name')
  678. self.require_match(self.CLOSE_PAREN, ')')
  679. def evaluate(self, stream, namespace, loader):
  680. template = loader.load_template(self.name.calculate(namespace, loader))
  681. ## TODO: local namespace?
  682. template.merge_to(namespace, stream, loader=loader)
  683. class StopDirective(_Element):
  684. STOP = re.compile(r'#stop\b(.*)', re.S + re.I)
  685. def parse(self):
  686. self.identity_match(self.STOP)
  687. def evaluate(self, stream, namespace, loader):
  688. if hasattr(stream, 'stop'):
  689. stream.stop = True
  690. # Represents a SINGLE user-defined directive
  691. class UserDefinedDirective(_Element):
  692. DIRECTIVES = []
  693. def parse(self):
  694. self.directive = self.next_element(self.DIRECTIVES)
  695. def evaluate(self, stream, namespace, loader):
  696. self.directive.evaluate(stream, namespace, loader)
  697. class SetDirective(_Element):
  698. START = re.compile(r'#set\b(.*)', re.S + re.I)
  699. def parse(self):
  700. self.identity_match(self.START)
  701. self.assignment = self.require_next_element(Assignment, 'assignment')
  702. def evaluate(self, stream, namespace, loader):
  703. self.assignment.evaluate(stream, namespace, loader)
  704. class ForeachDirective(_Element):
  705. START = re.compile(r'#foreach\b(.*)$', re.S + re.I)
  706. OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S)
  707. IN = re.compile(r'[ \t]+in[ \t]+(.*)$', re.S)
  708. LOOP_VAR_NAME = re.compile(r'\$([a-z_][a-z0-9_]*)(.*)$', re.S + re.I)
  709. CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S)
  710. def parse(self):
  711. ## Could be cleaner b/c syntax error if no '('
  712. self.identity_match(self.START)
  713. self.require_match(self.OPEN_PAREN, '(')
  714. self.loop_var_name, = self.require_match(self.LOOP_VAR_NAME, 'loop var name')
  715. self.require_match(self.IN, 'in')
  716. self.value = self.next_element(Value)
  717. self.require_match(self.CLOSE_PAREN, ')')
  718. self.block = self.next_element(Block)
  719. self.require_next_element(End, '#end')
  720. def evaluate(self, stream, namespace, loader):
  721. iterable = self.value.calculate(namespace, loader)
  722. counter = 1
  723. try:
  724. if iterable is None:
  725. return
  726. if hasattr(iterable, 'keys'): iterable = iterable.keys()
  727. if not hasattr(iterable, '__getitem__'):
  728. raise ValueError("value for $%s is not iterable in #foreach: %s" % (self.loop_var_name, iterable))
  729. for item in iterable:
  730. namespace = LocalNamespace(namespace)
  731. namespace['velocityCount'] = counter
  732. namespace[self.loop_var_name] = item
  733. self.block.evaluate(stream, namespace, loader)
  734. counter += 1
  735. except TypeError:
  736. raise
  737. class TemplateBody(_Element):
  738. def parse(self):
  739. self.block = self.next_element(Block)
  740. if self.next_text():
  741. raise self.syntax_error('block element')
  742. def evaluate(self, stream, namespace, loader):
  743. namespace = LocalNamespace(namespace)
  744. self.block.evaluate(stream, namespace, loader)
  745. class Block(_Element):
  746. def parse(self):
  747. self.children = []
  748. while True:
  749. try: self.children.append(self.next_element((Text, FormalReference, Comment, IfDirective, SetDirective,
  750. ForeachDirective, IncludeDirective, ParseDirective,
  751. MacroDefinition, StopDirective, UserDefinedDirective,
  752. MacroCall, FallthroughHashText)))
  753. except NoMatch: break
  754. def evaluate(self, stream, namespace, loader):
  755. for child in self.children:
  756. child.evaluate(stream, namespace, loader)