Show More
@@ -0,0 +1,155 b'' | |||||
|
1 | # template-filters.py - common template expansion filters | |||
|
2 | # | |||
|
3 | # Copyright 2005-2008 Matt Mackall <mpm@selenic.com> | |||
|
4 | # | |||
|
5 | # This software may be used and distributed according to the terms | |||
|
6 | # of the GNU General Public License, incorporated herein by reference. | |||
|
7 | ||||
|
8 | import cgi, re, os, time, urllib, textwrap | |||
|
9 | import util, templater | |||
|
10 | ||||
|
11 | agescales = [("second", 1), | |||
|
12 | ("minute", 60), | |||
|
13 | ("hour", 3600), | |||
|
14 | ("day", 3600 * 24), | |||
|
15 | ("week", 3600 * 24 * 7), | |||
|
16 | ("month", 3600 * 24 * 30), | |||
|
17 | ("year", 3600 * 24 * 365)] | |||
|
18 | ||||
|
19 | agescales.reverse() | |||
|
20 | ||||
|
21 | def age(date): | |||
|
22 | '''turn a (timestamp, tzoff) tuple into an age string.''' | |||
|
23 | ||||
|
24 | def plural(t, c): | |||
|
25 | if c == 1: | |||
|
26 | return t | |||
|
27 | return t + "s" | |||
|
28 | def fmt(t, c): | |||
|
29 | return "%d %s" % (c, plural(t, c)) | |||
|
30 | ||||
|
31 | now = time.time() | |||
|
32 | then = date[0] | |||
|
33 | delta = max(1, int(now - then)) | |||
|
34 | ||||
|
35 | for t, s in agescales: | |||
|
36 | n = delta / s | |||
|
37 | if n >= 2 or s == 1: | |||
|
38 | return fmt(t, n) | |||
|
39 | ||||
|
40 | para_re = None | |||
|
41 | space_re = None | |||
|
42 | ||||
|
43 | def fill(text, width): | |||
|
44 | '''fill many paragraphs.''' | |||
|
45 | global para_re, space_re | |||
|
46 | if para_re is None: | |||
|
47 | para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M) | |||
|
48 | space_re = re.compile(r' +') | |||
|
49 | ||||
|
50 | def findparas(): | |||
|
51 | start = 0 | |||
|
52 | while True: | |||
|
53 | m = para_re.search(text, start) | |||
|
54 | if not m: | |||
|
55 | w = len(text) | |||
|
56 | while w > start and text[w-1].isspace(): w -= 1 | |||
|
57 | yield text[start:w], text[w:] | |||
|
58 | break | |||
|
59 | yield text[start:m.start(0)], m.group(1) | |||
|
60 | start = m.end(1) | |||
|
61 | ||||
|
62 | return "".join([space_re.sub(' ', textwrap.fill(para, width)) + rest | |||
|
63 | for para, rest in findparas()]) | |||
|
64 | ||||
|
65 | def firstline(text): | |||
|
66 | '''return the first line of text''' | |||
|
67 | try: | |||
|
68 | return text.splitlines(1)[0].rstrip('\r\n') | |||
|
69 | except IndexError: | |||
|
70 | return '' | |||
|
71 | ||||
|
72 | def isodate(date): | |||
|
73 | '''turn a (timestamp, tzoff) tuple into an iso 8631 date and time.''' | |||
|
74 | return util.datestr(date, format='%Y-%m-%d %H:%M') | |||
|
75 | ||||
|
76 | def hgdate(date): | |||
|
77 | '''turn a (timestamp, tzoff) tuple into an hg cset timestamp.''' | |||
|
78 | return "%d %d" % date | |||
|
79 | ||||
|
80 | def nl2br(text): | |||
|
81 | '''replace raw newlines with xhtml line breaks.''' | |||
|
82 | return text.replace('\n', '<br/>\n') | |||
|
83 | ||||
|
84 | def obfuscate(text): | |||
|
85 | text = unicode(text, util._encoding, 'replace') | |||
|
86 | return ''.join(['&#%d;' % ord(c) for c in text]) | |||
|
87 | ||||
|
88 | def domain(author): | |||
|
89 | '''get domain of author, or empty string if none.''' | |||
|
90 | f = author.find('@') | |||
|
91 | if f == -1: return '' | |||
|
92 | author = author[f+1:] | |||
|
93 | f = author.find('>') | |||
|
94 | if f >= 0: author = author[:f] | |||
|
95 | return author | |||
|
96 | ||||
|
97 | def person(author): | |||
|
98 | '''get name of author, or else username.''' | |||
|
99 | f = author.find('<') | |||
|
100 | if f == -1: return util.shortuser(author) | |||
|
101 | return author[:f].rstrip() | |||
|
102 | ||||
|
103 | def shortdate(date): | |||
|
104 | '''turn (timestamp, tzoff) tuple into iso 8631 date.''' | |||
|
105 | return util.datestr(date, format='%Y-%m-%d', timezone=False) | |||
|
106 | ||||
|
107 | def indent(text, prefix): | |||
|
108 | '''indent each non-empty line of text after first with prefix.''' | |||
|
109 | lines = text.splitlines() | |||
|
110 | num_lines = len(lines) | |||
|
111 | def indenter(): | |||
|
112 | for i in xrange(num_lines): | |||
|
113 | l = lines[i] | |||
|
114 | if i and l.strip(): | |||
|
115 | yield prefix | |||
|
116 | yield l | |||
|
117 | if i < num_lines - 1 or text.endswith('\n'): | |||
|
118 | yield '\n' | |||
|
119 | return "".join(indenter()) | |||
|
120 | ||||
|
121 | def permissions(flags): | |||
|
122 | if "l" in flags: | |||
|
123 | return "lrwxrwxrwx" | |||
|
124 | if "x" in flags: | |||
|
125 | return "-rwxr-xr-x" | |||
|
126 | return "-rw-r--r--" | |||
|
127 | ||||
|
128 | filters = { | |||
|
129 | "addbreaks": nl2br, | |||
|
130 | "basename": os.path.basename, | |||
|
131 | "age": age, | |||
|
132 | "date": lambda x: util.datestr(x), | |||
|
133 | "domain": domain, | |||
|
134 | "email": util.email, | |||
|
135 | "escape": lambda x: cgi.escape(x, True), | |||
|
136 | "fill68": lambda x: fill(x, width=68), | |||
|
137 | "fill76": lambda x: fill(x, width=76), | |||
|
138 | "firstline": firstline, | |||
|
139 | "tabindent": lambda x: indent(x, '\t'), | |||
|
140 | "hgdate": hgdate, | |||
|
141 | "isodate": isodate, | |||
|
142 | "obfuscate": obfuscate, | |||
|
143 | "permissions": permissions, | |||
|
144 | "person": person, | |||
|
145 | "rfc822date": lambda x: util.datestr(x, "%a, %d %b %Y %H:%M:%S"), | |||
|
146 | "rfc3339date": lambda x: util.datestr(x, "%Y-%m-%dT%H:%M:%S", True, "%+03d:%02d"), | |||
|
147 | "short": lambda x: x[:12], | |||
|
148 | "shortdate": shortdate, | |||
|
149 | "stringify": templater.stringify, | |||
|
150 | "strip": lambda x: x.strip(), | |||
|
151 | "urlescape": lambda x: urllib.quote(x), | |||
|
152 | "user": lambda x: util.shortuser(x), | |||
|
153 | "stringescape": lambda x: x.encode('string_escape'), | |||
|
154 | } | |||
|
155 |
@@ -12,7 +12,7 b'' | |||||
12 | # <alias email> <actual email> |
|
12 | # <alias email> <actual email> | |
13 |
|
13 | |||
14 | from mercurial.i18n import gettext as _ |
|
14 | from mercurial.i18n import gettext as _ | |
15 | from mercurial import hg, mdiff, cmdutil, ui, util, templater, node |
|
15 | from mercurial import hg, mdiff, cmdutil, ui, util, templatefilters, node | |
16 | import os, sys |
|
16 | import os, sys | |
17 |
|
17 | |||
18 | def get_tty_width(): |
|
18 | def get_tty_width(): |
@@ -27,9 +27,9 b'' | |||||
27 |
|
27 | |||
28 | import re |
|
28 | import re | |
29 | from mercurial.hgweb import hgweb_mod |
|
29 | from mercurial.hgweb import hgweb_mod | |
30 | from mercurial import templater |
|
30 | from mercurial import templatefilters | |
31 |
|
31 | |||
32 |
orig_escape = templater. |
|
32 | orig_escape = templatefilters.filters["escape"] | |
33 |
|
33 | |||
34 | interhg_table = [] |
|
34 | interhg_table = [] | |
35 |
|
35 | |||
@@ -39,7 +39,7 b' def interhg_escape(x):' | |||||
39 | escstr = regexp.sub(format, escstr) |
|
39 | escstr = regexp.sub(format, escstr) | |
40 | return escstr |
|
40 | return escstr | |
41 |
|
41 | |||
42 |
templater. |
|
42 | templatefilters.filters["escape"] = interhg_escape | |
43 |
|
43 | |||
44 | orig_refresh = hgweb_mod.hgweb.refresh |
|
44 | orig_refresh = hgweb_mod.hgweb.refresh | |
45 |
|
45 |
@@ -78,8 +78,8 b" like CVS' $Log$, are not supported. A ke" | |||||
78 | "Log = {desc}" expands to the first line of the changeset description. |
|
78 | "Log = {desc}" expands to the first line of the changeset description. | |
79 | ''' |
|
79 | ''' | |
80 |
|
80 | |||
81 | from mercurial import commands, cmdutil, context, dispatch, filelog |
|
81 | from mercurial import commands, cmdutil, context, dispatch, filelog, revlog | |
82 |
from mercurial import patch, localrepo, |
|
82 | from mercurial import patch, localrepo, templater, templatefilters, util | |
83 | from mercurial.node import * |
|
83 | from mercurial.node import * | |
84 | from mercurial.i18n import _ |
|
84 | from mercurial.i18n import _ | |
85 | import re, shutil, sys, tempfile, time |
|
85 | import re, shutil, sys, tempfile, time | |
@@ -130,7 +130,7 b' class kwtemplater(object):' | |||||
130 | kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped) |
|
130 | kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped) | |
131 | self.re_kw = re.compile(kwpat) |
|
131 | self.re_kw = re.compile(kwpat) | |
132 |
|
132 | |||
133 |
templater. |
|
133 | templatefilters.filters['utcdate'] = utcdate | |
134 | self.ct = cmdutil.changeset_templater(self.ui, self.repo, |
|
134 | self.ct = cmdutil.changeset_templater(self.ui, self.repo, | |
135 | False, '', False) |
|
135 | False, '', False) | |
136 |
|
136 | |||
@@ -149,7 +149,8 b' class kwtemplater(object):' | |||||
149 | self.ct.use_template(self.templates[kw]) |
|
149 | self.ct.use_template(self.templates[kw]) | |
150 | self.ui.pushbuffer() |
|
150 | self.ui.pushbuffer() | |
151 | self.ct.show(changenode=fnode, root=self.repo.root, file=self.path) |
|
151 | self.ct.show(changenode=fnode, root=self.repo.root, file=self.path) | |
152 |
return '$%s: %s $' % (kw, templater.firstline( |
|
152 | return '$%s: %s $' % (kw, templatefilters.firstline( | |
|
153 | self.ui.popbuffer())) | |||
153 |
|
154 | |||
154 | return subfunc(kwsub, data) |
|
155 | return subfunc(kwsub, data) | |
155 |
|
156 |
@@ -8,7 +8,7 b'' | |||||
8 | from node import * |
|
8 | from node import * | |
9 | from i18n import _ |
|
9 | from i18n import _ | |
10 | import os, sys, bisect, stat |
|
10 | import os, sys, bisect, stat | |
11 | import mdiff, bdiff, util, templater, patch, errno |
|
11 | import mdiff, bdiff, util, templater, templatefilters, patch, errno | |
12 |
|
12 | |||
13 | revrangesep = ':' |
|
13 | revrangesep = ':' | |
14 |
|
14 | |||
@@ -673,7 +673,7 b' class changeset_templater(changeset_prin' | |||||
673 |
|
673 | |||
674 | def __init__(self, ui, repo, patch, mapfile, buffered): |
|
674 | def __init__(self, ui, repo, patch, mapfile, buffered): | |
675 | changeset_printer.__init__(self, ui, repo, patch, buffered) |
|
675 | changeset_printer.__init__(self, ui, repo, patch, buffered) | |
676 |
filters = templater. |
|
676 | filters = templatefilters.filters.copy() | |
677 | filters['formatnode'] = (ui.debugflag and (lambda x: x) |
|
677 | filters['formatnode'] = (ui.debugflag and (lambda x: x) | |
678 | or (lambda x: x[:12])) |
|
678 | or (lambda x: x[:12])) | |
679 | self.t = templater.templater(mapfile, filters, |
|
679 | self.t = templater.templater(mapfile, filters, |
@@ -9,7 +9,7 b'' | |||||
9 | import os, mimetypes, re |
|
9 | import os, mimetypes, re | |
10 | from mercurial.node import * |
|
10 | from mercurial.node import * | |
11 | from mercurial import mdiff, ui, hg, util, archival, patch, hook |
|
11 | from mercurial import mdiff, ui, hg, util, archival, patch, hook | |
12 | from mercurial import revlog, templater |
|
12 | from mercurial import revlog, templater, templatefilters | |
13 | from common import ErrorResponse, get_mtime, style_map, paritygen, get_contact |
|
13 | from common import ErrorResponse, get_mtime, style_map, paritygen, get_contact | |
14 | from request import wsgirequest |
|
14 | from request import wsgirequest | |
15 | import webcommands, protocol |
|
15 | import webcommands, protocol | |
@@ -288,7 +288,7 b' class hgweb(object):' | |||||
288 |
|
288 | |||
289 | # create the templater |
|
289 | # create the templater | |
290 |
|
290 | |||
291 |
tmpl = templater.templater(mapfile, templater. |
|
291 | tmpl = templater.templater(mapfile, templatefilters.filters, | |
292 | defaults={"url": req.url, |
|
292 | defaults={"url": req.url, | |
293 | "staticurl": staticurl, |
|
293 | "staticurl": staticurl, | |
294 | "urlbase": urlbase, |
|
294 | "urlbase": urlbase, |
@@ -8,8 +8,8 b'' | |||||
8 |
|
8 | |||
9 | import os |
|
9 | import os | |
10 | from mercurial.i18n import gettext as _ |
|
10 | from mercurial.i18n import gettext as _ | |
11 | from mercurial import ui, hg, util, templater |
|
11 | from mercurial import ui, hg, util, templater, templatefilters | |
12 |
from common import ErrorResponse, get_mtime, staticfile, style_map, paritygen, |
|
12 | from common import ErrorResponse, get_mtime, staticfile, style_map, paritygen,\ | |
13 | get_contact |
|
13 | get_contact | |
14 | from hgweb_mod import hgweb |
|
14 | from hgweb_mod import hgweb | |
15 | from request import wsgirequest |
|
15 | from request import wsgirequest | |
@@ -266,7 +266,7 b' class hgwebdir(object):' | |||||
266 | if self.stripecount is None: |
|
266 | if self.stripecount is None: | |
267 | self.stripecount = int(config('web', 'stripes', 1)) |
|
267 | self.stripecount = int(config('web', 'stripes', 1)) | |
268 | mapfile = style_map(templater.templatepath(), style) |
|
268 | mapfile = style_map(templater.templatepath(), style) | |
269 |
tmpl = templater.templater(mapfile, templater. |
|
269 | tmpl = templater.templater(mapfile, templatefilters.filters, | |
270 | defaults={"header": header, |
|
270 | defaults={"header": header, | |
271 | "footer": footer, |
|
271 | "footer": footer, | |
272 | "motd": motd, |
|
272 | "motd": motd, |
@@ -6,7 +6,7 b'' | |||||
6 | # of the GNU General Public License, incorporated herein by reference. |
|
6 | # of the GNU General Public License, incorporated herein by reference. | |
7 |
|
7 | |||
8 | from i18n import _ |
|
8 | from i18n import _ | |
9 | import cgi, re, sys, os, time, urllib, util, textwrap |
|
9 | import re, sys, os | |
10 |
|
10 | |||
11 | def parsestring(s, quoted=True): |
|
11 | def parsestring(s, quoted=True): | |
12 | '''parse a string using simple c-like syntax. |
|
12 | '''parse a string using simple c-like syntax. | |
@@ -122,157 +122,6 b' class templater(object):' | |||||
122 | v = self.filters[f](v) |
|
122 | v = self.filters[f](v) | |
123 | yield v |
|
123 | yield v | |
124 |
|
124 | |||
125 | agescales = [("second", 1), |
|
|||
126 | ("minute", 60), |
|
|||
127 | ("hour", 3600), |
|
|||
128 | ("day", 3600 * 24), |
|
|||
129 | ("week", 3600 * 24 * 7), |
|
|||
130 | ("month", 3600 * 24 * 30), |
|
|||
131 | ("year", 3600 * 24 * 365)] |
|
|||
132 |
|
||||
133 | agescales.reverse() |
|
|||
134 |
|
||||
135 | def age(date): |
|
|||
136 | '''turn a (timestamp, tzoff) tuple into an age string.''' |
|
|||
137 |
|
||||
138 | def plural(t, c): |
|
|||
139 | if c == 1: |
|
|||
140 | return t |
|
|||
141 | return t + "s" |
|
|||
142 | def fmt(t, c): |
|
|||
143 | return "%d %s" % (c, plural(t, c)) |
|
|||
144 |
|
||||
145 | now = time.time() |
|
|||
146 | then = date[0] |
|
|||
147 | delta = max(1, int(now - then)) |
|
|||
148 |
|
||||
149 | for t, s in agescales: |
|
|||
150 | n = delta / s |
|
|||
151 | if n >= 2 or s == 1: |
|
|||
152 | return fmt(t, n) |
|
|||
153 |
|
||||
154 | def stringify(thing): |
|
|||
155 | '''turn nested template iterator into string.''' |
|
|||
156 | if hasattr(thing, '__iter__'): |
|
|||
157 | return "".join([stringify(t) for t in thing if t is not None]) |
|
|||
158 | return str(thing) |
|
|||
159 |
|
||||
160 | para_re = None |
|
|||
161 | space_re = None |
|
|||
162 |
|
||||
163 | def fill(text, width): |
|
|||
164 | '''fill many paragraphs.''' |
|
|||
165 | global para_re, space_re |
|
|||
166 | if para_re is None: |
|
|||
167 | para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M) |
|
|||
168 | space_re = re.compile(r' +') |
|
|||
169 |
|
||||
170 | def findparas(): |
|
|||
171 | start = 0 |
|
|||
172 | while True: |
|
|||
173 | m = para_re.search(text, start) |
|
|||
174 | if not m: |
|
|||
175 | w = len(text) |
|
|||
176 | while w > start and text[w-1].isspace(): w -= 1 |
|
|||
177 | yield text[start:w], text[w:] |
|
|||
178 | break |
|
|||
179 | yield text[start:m.start(0)], m.group(1) |
|
|||
180 | start = m.end(1) |
|
|||
181 |
|
||||
182 | return "".join([space_re.sub(' ', textwrap.fill(para, width)) + rest |
|
|||
183 | for para, rest in findparas()]) |
|
|||
184 |
|
||||
185 | def firstline(text): |
|
|||
186 | '''return the first line of text''' |
|
|||
187 | try: |
|
|||
188 | return text.splitlines(1)[0].rstrip('\r\n') |
|
|||
189 | except IndexError: |
|
|||
190 | return '' |
|
|||
191 |
|
||||
192 | def isodate(date): |
|
|||
193 | '''turn a (timestamp, tzoff) tuple into an iso 8631 date and time.''' |
|
|||
194 | return util.datestr(date, format='%Y-%m-%d %H:%M') |
|
|||
195 |
|
||||
196 | def hgdate(date): |
|
|||
197 | '''turn a (timestamp, tzoff) tuple into an hg cset timestamp.''' |
|
|||
198 | return "%d %d" % date |
|
|||
199 |
|
||||
200 | def nl2br(text): |
|
|||
201 | '''replace raw newlines with xhtml line breaks.''' |
|
|||
202 | return text.replace('\n', '<br/>\n') |
|
|||
203 |
|
||||
204 | def obfuscate(text): |
|
|||
205 | text = unicode(text, util._encoding, 'replace') |
|
|||
206 | return ''.join(['&#%d;' % ord(c) for c in text]) |
|
|||
207 |
|
||||
208 | def domain(author): |
|
|||
209 | '''get domain of author, or empty string if none.''' |
|
|||
210 | f = author.find('@') |
|
|||
211 | if f == -1: return '' |
|
|||
212 | author = author[f+1:] |
|
|||
213 | f = author.find('>') |
|
|||
214 | if f >= 0: author = author[:f] |
|
|||
215 | return author |
|
|||
216 |
|
||||
217 | def person(author): |
|
|||
218 | '''get name of author, or else username.''' |
|
|||
219 | f = author.find('<') |
|
|||
220 | if f == -1: return util.shortuser(author) |
|
|||
221 | return author[:f].rstrip() |
|
|||
222 |
|
||||
223 | def shortdate(date): |
|
|||
224 | '''turn (timestamp, tzoff) tuple into iso 8631 date.''' |
|
|||
225 | return util.datestr(date, format='%Y-%m-%d', timezone=False) |
|
|||
226 |
|
||||
227 | def indent(text, prefix): |
|
|||
228 | '''indent each non-empty line of text after first with prefix.''' |
|
|||
229 | lines = text.splitlines() |
|
|||
230 | num_lines = len(lines) |
|
|||
231 | def indenter(): |
|
|||
232 | for i in xrange(num_lines): |
|
|||
233 | l = lines[i] |
|
|||
234 | if i and l.strip(): |
|
|||
235 | yield prefix |
|
|||
236 | yield l |
|
|||
237 | if i < num_lines - 1 or text.endswith('\n'): |
|
|||
238 | yield '\n' |
|
|||
239 | return "".join(indenter()) |
|
|||
240 |
|
||||
241 | def permissions(flags): |
|
|||
242 | if "l" in flags: |
|
|||
243 | return "lrwxrwxrwx" |
|
|||
244 | if "x" in flags: |
|
|||
245 | return "-rwxr-xr-x" |
|
|||
246 | return "-rw-r--r--" |
|
|||
247 |
|
||||
248 | common_filters = { |
|
|||
249 | "addbreaks": nl2br, |
|
|||
250 | "basename": os.path.basename, |
|
|||
251 | "age": age, |
|
|||
252 | "date": lambda x: util.datestr(x), |
|
|||
253 | "domain": domain, |
|
|||
254 | "email": util.email, |
|
|||
255 | "escape": lambda x: cgi.escape(x, True), |
|
|||
256 | "fill68": lambda x: fill(x, width=68), |
|
|||
257 | "fill76": lambda x: fill(x, width=76), |
|
|||
258 | "firstline": firstline, |
|
|||
259 | "tabindent": lambda x: indent(x, '\t'), |
|
|||
260 | "hgdate": hgdate, |
|
|||
261 | "isodate": isodate, |
|
|||
262 | "obfuscate": obfuscate, |
|
|||
263 | "permissions": permissions, |
|
|||
264 | "person": person, |
|
|||
265 | "rfc822date": lambda x: util.datestr(x, "%a, %d %b %Y %H:%M:%S"), |
|
|||
266 | "rfc3339date": lambda x: util.datestr(x, "%Y-%m-%dT%H:%M:%S", True, "%+03d:%02d"), |
|
|||
267 | "short": lambda x: x[:12], |
|
|||
268 | "shortdate": shortdate, |
|
|||
269 | "stringify": stringify, |
|
|||
270 | "strip": lambda x: x.strip(), |
|
|||
271 | "urlescape": lambda x: urllib.quote(x), |
|
|||
272 | "user": lambda x: util.shortuser(x), |
|
|||
273 | "stringescape": lambda x: x.encode('string_escape'), |
|
|||
274 | } |
|
|||
275 |
|
||||
276 | def templatepath(name=None): |
|
125 | def templatepath(name=None): | |
277 | '''return location of template file or directory (if no name). |
|
126 | '''return location of template file or directory (if no name). | |
278 | returns None if not found.''' |
|
127 | returns None if not found.''' | |
@@ -289,3 +138,9 b' def templatepath(name=None):' | |||||
289 | if (name and os.path.exists(p)) or os.path.isdir(p): |
|
138 | if (name and os.path.exists(p)) or os.path.isdir(p): | |
290 | return os.path.normpath(p) |
|
139 | return os.path.normpath(p) | |
291 |
|
140 | |||
|
141 | def stringify(thing): | |||
|
142 | '''turn nested template iterator into string.''' | |||
|
143 | if hasattr(thing, '__iter__'): | |||
|
144 | return "".join([stringify(t) for t in thing if t is not None]) | |||
|
145 | return str(thing) | |||
|
146 |
General Comments 0
You need to be logged in to leave comments.
Login now