##// END OF EJS Templates
templater: keep a cache of Python functions for filter expressions
Dirkjan Ochtman -
r8476:d5dda64f default
parent child Browse files
Show More
@@ -1,235 +1,242 b''
1 # templater.py - template expansion for output
1 # templater.py - template expansion for output
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 from i18n import _
8 from i18n import _
9 import re, sys, os
9 import re, sys, os
10 import util, config, templatefilters
10 import util, config, templatefilters
11
11
12 path = ['templates', '../templates']
12 path = ['templates', '../templates']
13 stringify = templatefilters.stringify
13 stringify = templatefilters.stringify
14
14
15 def parsestring(s, quoted=True):
15 def parsestring(s, quoted=True):
16 '''parse a string using simple c-like syntax.
16 '''parse a string using simple c-like syntax.
17 string must be in quotes if quoted is True.'''
17 string must be in quotes if quoted is True.'''
18 if quoted:
18 if quoted:
19 if len(s) < 2 or s[0] != s[-1]:
19 if len(s) < 2 or s[0] != s[-1]:
20 raise SyntaxError(_('unmatched quotes'))
20 raise SyntaxError(_('unmatched quotes'))
21 return s[1:-1].decode('string_escape')
21 return s[1:-1].decode('string_escape')
22
22
23 return s.decode('string_escape')
23 return s.decode('string_escape')
24
24
25 class engine(object):
25 class engine(object):
26 '''template expansion engine.
26 '''template expansion engine.
27
27
28 template expansion works like this. a map file contains key=value
28 template expansion works like this. a map file contains key=value
29 pairs. if value is quoted, it is treated as string. otherwise, it
29 pairs. if value is quoted, it is treated as string. otherwise, it
30 is treated as name of template file.
30 is treated as name of template file.
31
31
32 templater is asked to expand a key in map. it looks up key, and
32 templater is asked to expand a key in map. it looks up key, and
33 looks for strings like this: {foo}. it expands {foo} by looking up
33 looks for strings like this: {foo}. it expands {foo} by looking up
34 foo in map, and substituting it. expansion is recursive: it stops
34 foo in map, and substituting it. expansion is recursive: it stops
35 when there is no more {foo} to replace.
35 when there is no more {foo} to replace.
36
36
37 expansion also allows formatting and filtering.
37 expansion also allows formatting and filtering.
38
38
39 format uses key to expand each item in list. syntax is
39 format uses key to expand each item in list. syntax is
40 {key%format}.
40 {key%format}.
41
41
42 filter uses function to transform value. syntax is
42 filter uses function to transform value. syntax is
43 {key|filter1|filter2|...}.'''
43 {key|filter1|filter2|...}.'''
44
44
45 template_re = re.compile(r'{([\w\|%]+)}|#([\w\|%]+)#')
45 template_re = re.compile(r'{([\w\|%]+)}|#([\w\|%]+)#')
46
46
47 def __init__(self, loader, filters={}, defaults={}):
47 def __init__(self, loader, filters={}, defaults={}):
48 self.loader = loader
48 self.loader = loader
49 self.filters = filters
49 self.filters = filters
50 self.defaults = defaults
50 self.defaults = defaults
51 self.cache = {}
51
52
52 def process(self, t, map):
53 def process(self, t, map):
53 '''Perform expansion. t is name of map element to expand. map contains
54 '''Perform expansion. t is name of map element to expand. map contains
54 added elements for use during expansion. Is a generator.'''
55 added elements for use during expansion. Is a generator.'''
55 tmpl = self.loader(t)
56 tmpl = self.loader(t)
56 iters = [self._process(tmpl, map)]
57 iters = [self._process(tmpl, map)]
57 while iters:
58 while iters:
58 try:
59 try:
59 item = iters[0].next()
60 item = iters[0].next()
60 except StopIteration:
61 except StopIteration:
61 iters.pop(0)
62 iters.pop(0)
62 continue
63 continue
63 if isinstance(item, str):
64 if isinstance(item, str):
64 yield item
65 yield item
65 elif item is None:
66 elif item is None:
66 yield ''
67 yield ''
67 elif hasattr(item, '__iter__'):
68 elif hasattr(item, '__iter__'):
68 iters.insert(0, iter(item))
69 iters.insert(0, iter(item))
69 else:
70 else:
70 yield str(item)
71 yield str(item)
71
72
72 def _format(self, expr, get, map):
73 def _format(self, expr, get, map):
73 key, format = expr.split('%')
74 key, format = expr.split('%')
74 v = get(key)
75 v = get(key)
75 if not hasattr(v, '__iter__'):
76 if not hasattr(v, '__iter__'):
76 raise SyntaxError(_("Error expanding '%s%%%s'") % (key, format))
77 raise SyntaxError(_("Error expanding '%s%%%s'") % (key, format))
77 lm = map.copy()
78 lm = map.copy()
78 for i in v:
79 for i in v:
79 lm.update(i)
80 lm.update(i)
80 yield self.process(format, lm)
81 yield self.process(format, lm)
81
82
82 def _filter(self, expr, get, map):
83 def _filter(self, expr, get, map):
83 parts = expr.split('|')
84 if expr not in self.cache:
84 v = get(parts[0])
85 parts = expr.split('|')
85 for f in parts[1:]:
86 filters = parts[1:]
86 v = self.filters[f](v)
87 for f in filters:
87 return v
88 if f not in self.filters:
89 raise SyntaxError(_("unknown filter '%s'") % expr)
90 calls = '('.join(i for i in reversed(filters))
91 end = ')' * len(filters)
92 code = "lambda _get: %s(_get('%s')%s" % (calls, parts[0], end)
93 self.cache[expr] = eval(code, self.filters)
94 return self.cache[expr](get)
88
95
89 def _process(self, tmpl, map):
96 def _process(self, tmpl, map):
90 '''Render a template. Returns a generator.'''
97 '''Render a template. Returns a generator.'''
91
98
92 def get(key):
99 def get(key):
93 v = map.get(key)
100 v = map.get(key)
94 if v is None:
101 if v is None:
95 v = self.defaults.get(key, '')
102 v = self.defaults.get(key, '')
96 if hasattr(v, '__call__'):
103 if hasattr(v, '__call__'):
97 v = v(**map)
104 v = v(**map)
98 return v
105 return v
99
106
100 while tmpl:
107 while tmpl:
101 m = self.template_re.search(tmpl)
108 m = self.template_re.search(tmpl)
102 if not m:
109 if not m:
103 yield tmpl
110 yield tmpl
104 break
111 break
105
112
106 start, end = m.span(0)
113 start, end = m.span(0)
107 variants = m.groups()
114 variants = m.groups()
108 expr = variants[0] or variants[1]
115 expr = variants[0] or variants[1]
109
116
110 if start:
117 if start:
111 yield tmpl[:start]
118 yield tmpl[:start]
112 tmpl = tmpl[end:]
119 tmpl = tmpl[end:]
113
120
114 if '%' in expr:
121 if '%' in expr:
115 yield self._format(expr, get, map)
122 yield self._format(expr, get, map)
116 elif '|' in expr:
123 elif '|' in expr:
117 yield self._filter(expr, get, map)
124 yield self._filter(expr, get, map)
118 else:
125 else:
119 yield get(expr)
126 yield get(expr)
120
127
121 engines = {'default': engine}
128 engines = {'default': engine}
122
129
123 class templater(object):
130 class templater(object):
124
131
125 def __init__(self, mapfile, filters={}, defaults={}, cache={},
132 def __init__(self, mapfile, filters={}, defaults={}, cache={},
126 minchunk=1024, maxchunk=65536):
133 minchunk=1024, maxchunk=65536):
127 '''set up template engine.
134 '''set up template engine.
128 mapfile is name of file to read map definitions from.
135 mapfile is name of file to read map definitions from.
129 filters is dict of functions. each transforms a value into another.
136 filters is dict of functions. each transforms a value into another.
130 defaults is dict of default map definitions.'''
137 defaults is dict of default map definitions.'''
131 self.mapfile = mapfile or 'template'
138 self.mapfile = mapfile or 'template'
132 self.cache = cache.copy()
139 self.cache = cache.copy()
133 self.map = {}
140 self.map = {}
134 self.base = (mapfile and os.path.dirname(mapfile)) or ''
141 self.base = (mapfile and os.path.dirname(mapfile)) or ''
135 self.filters = templatefilters.filters.copy()
142 self.filters = templatefilters.filters.copy()
136 self.filters.update(filters)
143 self.filters.update(filters)
137 self.defaults = defaults
144 self.defaults = defaults
138 self.minchunk, self.maxchunk = minchunk, maxchunk
145 self.minchunk, self.maxchunk = minchunk, maxchunk
139 self.engines = {}
146 self.engines = {}
140
147
141 if not mapfile:
148 if not mapfile:
142 return
149 return
143 if not os.path.exists(mapfile):
150 if not os.path.exists(mapfile):
144 raise util.Abort(_('style not found: %s') % mapfile)
151 raise util.Abort(_('style not found: %s') % mapfile)
145
152
146 conf = config.config()
153 conf = config.config()
147 conf.read(mapfile)
154 conf.read(mapfile)
148
155
149 for key, val in conf[''].items():
156 for key, val in conf[''].items():
150 if val[0] in "'\"":
157 if val[0] in "'\"":
151 try:
158 try:
152 self.cache[key] = parsestring(val)
159 self.cache[key] = parsestring(val)
153 except SyntaxError, inst:
160 except SyntaxError, inst:
154 raise SyntaxError('%s: %s' %
161 raise SyntaxError('%s: %s' %
155 (conf.source('', key), inst.args[0]))
162 (conf.source('', key), inst.args[0]))
156 else:
163 else:
157 val = 'default', val
164 val = 'default', val
158 if ':' in val[1]:
165 if ':' in val[1]:
159 val = val[1].split(':', 1)
166 val = val[1].split(':', 1)
160 self.map[key] = val[0], os.path.join(self.base, val[1])
167 self.map[key] = val[0], os.path.join(self.base, val[1])
161
168
162 def __contains__(self, key):
169 def __contains__(self, key):
163 return key in self.cache or key in self.map
170 return key in self.cache or key in self.map
164
171
165 def load(self, t):
172 def load(self, t):
166 '''Get the template for the given template name. Use a local cache.'''
173 '''Get the template for the given template name. Use a local cache.'''
167 if not t in self.cache:
174 if not t in self.cache:
168 try:
175 try:
169 self.cache[t] = open(self.map[t][1]).read()
176 self.cache[t] = open(self.map[t][1]).read()
170 except IOError, inst:
177 except IOError, inst:
171 raise IOError(inst.args[0], _('template file %s: %s') %
178 raise IOError(inst.args[0], _('template file %s: %s') %
172 (self.map[t][1], inst.args[1]))
179 (self.map[t][1], inst.args[1]))
173 return self.cache[t]
180 return self.cache[t]
174
181
175 def __call__(self, t, **map):
182 def __call__(self, t, **map):
176 ttype = t in self.map and self.map[t][0] or 'default'
183 ttype = t in self.map and self.map[t][0] or 'default'
177 proc = self.engines.get(ttype)
184 proc = self.engines.get(ttype)
178 if proc is None:
185 if proc is None:
179 proc = engines[ttype](self.load, self.filters, self.defaults)
186 proc = engines[ttype](self.load, self.filters, self.defaults)
180 self.engines[ttype] = proc
187 self.engines[ttype] = proc
181
188
182 stream = proc.process(t, map)
189 stream = proc.process(t, map)
183 if self.minchunk:
190 if self.minchunk:
184 stream = util.increasingchunks(stream, min=self.minchunk,
191 stream = util.increasingchunks(stream, min=self.minchunk,
185 max=self.maxchunk)
192 max=self.maxchunk)
186 return stream
193 return stream
187
194
188 def templatepath(name=None):
195 def templatepath(name=None):
189 '''return location of template file or directory (if no name).
196 '''return location of template file or directory (if no name).
190 returns None if not found.'''
197 returns None if not found.'''
191 normpaths = []
198 normpaths = []
192
199
193 # executable version (py2exe) doesn't support __file__
200 # executable version (py2exe) doesn't support __file__
194 if hasattr(sys, 'frozen'):
201 if hasattr(sys, 'frozen'):
195 module = sys.executable
202 module = sys.executable
196 else:
203 else:
197 module = __file__
204 module = __file__
198 for f in path:
205 for f in path:
199 if f.startswith('/'):
206 if f.startswith('/'):
200 p = f
207 p = f
201 else:
208 else:
202 fl = f.split('/')
209 fl = f.split('/')
203 p = os.path.join(os.path.dirname(module), *fl)
210 p = os.path.join(os.path.dirname(module), *fl)
204 if name:
211 if name:
205 p = os.path.join(p, name)
212 p = os.path.join(p, name)
206 if name and os.path.exists(p):
213 if name and os.path.exists(p):
207 return os.path.normpath(p)
214 return os.path.normpath(p)
208 elif os.path.isdir(p):
215 elif os.path.isdir(p):
209 normpaths.append(os.path.normpath(p))
216 normpaths.append(os.path.normpath(p))
210
217
211 return normpaths
218 return normpaths
212
219
213 def stylemap(style, paths=None):
220 def stylemap(style, paths=None):
214 """Return path to mapfile for a given style.
221 """Return path to mapfile for a given style.
215
222
216 Searches mapfile in the following locations:
223 Searches mapfile in the following locations:
217 1. templatepath/style/map
224 1. templatepath/style/map
218 2. templatepath/map-style
225 2. templatepath/map-style
219 3. templatepath/map
226 3. templatepath/map
220 """
227 """
221
228
222 if paths is None:
229 if paths is None:
223 paths = templatepath()
230 paths = templatepath()
224 elif isinstance(paths, str):
231 elif isinstance(paths, str):
225 paths = [paths]
232 paths = [paths]
226
233
227 locations = style and [os.path.join(style, "map"), "map-" + style] or []
234 locations = style and [os.path.join(style, "map"), "map-" + style] or []
228 locations.append("map")
235 locations.append("map")
229 for path in paths:
236 for path in paths:
230 for location in locations:
237 for location in locations:
231 mapfile = os.path.join(path, location)
238 mapfile = os.path.join(path, location)
232 if os.path.isfile(mapfile):
239 if os.path.isfile(mapfile):
233 return mapfile
240 return mapfile
234
241
235 raise RuntimeError("No hgweb templates found in %r" % paths)
242 raise RuntimeError("No hgweb templates found in %r" % paths)
General Comments 0
You need to be logged in to leave comments. Login now