Status: | Merged |
---|---|
Merged at revision: | not available |
Proposed branch: | lp:~corydodt/txgenshi/502823 |
Merge into: | lp:txgenshi |
Diff against target: |
370 lines (+220/-48) 5 files modified
demo/templates/genshimix.xhtml (+7/-0) txgenshi/loader.py (+91/-35) txgenshi/test/example.py (+10/-8) txgenshi/test/test_example.py (+22/-5) txgenshi/test/test_loader.py (+90/-0) |
To merge this branch: | bzr merge lp:~corydodt/txgenshi/502823 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Duncan McGreggor | Approve | ||
Review via email: mp+17116@code.launchpad.net |
This proposal supersedes a proposal from 2010-01-10.
Commit message
Description of the change
Cory Dodt (corydodt) wrote : Posted in a previous version of this proposal | # |
Duncan McGreggor (oubiwann) wrote : | # |
Sweeeeet.
/me continues to be super-stoked about your contributions :-) They make me want to do web-dev again ;-)
+1 for merge.
Duncan McGreggor (oubiwann) wrote : | # |
Don't know if you use ohloh.net at all, but I just kodoed you for the work you've done here :-)
https:/
Cory Dodt (corydodt) wrote : | # |
That's cool, thanks! :-)
C
On Mon, Jan 11, 2010 at 9:39 AM, Duncan McGreggor <email address hidden>wrote:
> Don't know if you use ohloh.net at all, but I just kodoed you for the work
> you've done here :-)
>
> https:/
> --
> https:/
> You are the owner of lp:~corydodt/txgenshi/502823.
>
--
_______
Cory Dodt (corydodt) wrote : | # |
One back at you for writing txGenshi in the first place. It's exciting to
have an alternative to Nevow templates usable inside Nevow. The templates
were the only thing I've never been satisfied with in that package.
C
On Mon, Jan 11, 2010 at 10:54 AM, Cory Dodt <email address hidden>wrote:
> That's cool, thanks! :-)
>
> C
>
>
> On Mon, Jan 11, 2010 at 9:39 AM, Duncan McGreggor <<email address hidden>
> >wrote:
>
> > Don't know if you use ohloh.net at all, but I just kodoed you for the
> work
> > you've done here :-)
> >
> > https:/
> > --
> > https:/
> > You are the owner of lp:~corydodt/txgenshi/502823.
> >
>
>
>
> --
> _______
>
> https:/
> You are the owner of lp:~corydodt/txgenshi/502823.
>
--
_______
Preview Diff
1 | === modified file 'demo/templates/genshimix.xhtml' |
2 | --- demo/templates/genshimix.xhtml 2010-01-07 18:29:05 +0000 |
3 | +++ demo/templates/genshimix.xhtml 2010-01-11 01:04:10 +0000 |
4 | @@ -115,6 +115,13 @@ |
5 | <b>foo</b> |
6 | </div> |
7 | </div> |
8 | + <div genshi:choose=""> |
9 | + <h2>Genshi embedded functions</h2> |
10 | + <div genshi:when="not defined('ninjaKitteh')" |
11 | + genshi:with="dude=value_of('ninjaKitteh', username)"> |
12 | + <b>Ninja Kitteh is not defined, $dude</b> |
13 | + </div> |
14 | + </div> |
15 | <div> |
16 | <h2>Genshi XInclude</h2> |
17 | <xi:include href="templates/genshiinclude.xhtml"></xi:include> |
18 | |
19 | === modified file 'txgenshi/loader.py' |
20 | --- txgenshi/loader.py 2010-01-07 18:54:19 +0000 |
21 | +++ txgenshi/loader.py 2010-01-11 01:04:10 +0000 |
22 | @@ -1,44 +1,99 @@ |
23 | +""" |
24 | +Nevow-compatible loader implementation that evaluates its template against |
25 | +Genshi, using the Resource itself as the data source. |
26 | + |
27 | +- Any attribute defined in the Resource which uses genshifile or genshistr as |
28 | + its loader will be available as a Genshi variable |
29 | +- Any attribute which is callable may be used as a variable without adding |
30 | + parentheses to call it, and it will be called when it is evaluated. Any |
31 | + such variable MUST NOT take any required arguments. |
32 | +- Any variable used in a template is FROZEN (cached) the first time it is |
33 | + used. This means you should not expect any the evaluation of any Genshi |
34 | + variable to change between accesses if it is used more than once in a |
35 | + template. The exception to this rule is callables, which will be called |
36 | + each time they are accessed. |
37 | +""" |
38 | + |
39 | from inspect import getmembers |
40 | +from collections import deque |
41 | |
42 | -from nevow import rend |
43 | from nevow import inevow |
44 | from nevow.loaders import xmlstr |
45 | |
46 | -from genshi.template import MarkupTemplate |
47 | - |
48 | -# get a list of attributes that the rend.Page object has |
49 | -rendPageAttrs = [x[0] for x in getmembers(rend.Page)] |
50 | - |
51 | - |
52 | -def getObjectData(obj): |
53 | - data = {} |
54 | - for attr, val in getmembers(obj): |
55 | - assert isinstance(attr, basestring), "Member name is not a string!" |
56 | - # skip non-public methods |
57 | - if attr.startswith('_'): |
58 | - continue |
59 | - # skip custom Nevow methods |
60 | - if attr.startswith('child_'): |
61 | - continue |
62 | - if attr.startswith('render_'): |
63 | - continue |
64 | - if attr.startswith('data_'): |
65 | - continue |
66 | - # skip nevow.rend.Page methods |
67 | - if attr in rendPageAttrs: |
68 | - continue |
69 | - member = getattr(obj, attr) |
70 | - if callable(member): |
71 | - val = member() |
72 | - try: |
73 | - data[attr] = val |
74 | - except Exception, err: # pragma: nocover |
75 | - import pdb;pdb.set_trace() # pragma: nocover |
76 | - return data |
77 | +from genshi.template import MarkupTemplate, Context |
78 | + |
79 | + |
80 | +class TxGenshiContext(Context): |
81 | + """ |
82 | + Subclass Context to get rid of the awful **kwargs initializer, which |
83 | + prevents us from specifying our own template variables flexibly. |
84 | + """ |
85 | + def __init__(self, data): |
86 | + self.frames = deque([data]) |
87 | + self.pop = self.frames.popleft |
88 | + self.push = self.frames.appendleft |
89 | + self._match_templates = [] |
90 | + self._choice_stack = [] |
91 | + |
92 | + # Helper functions for use in expressions |
93 | + def defined(name): |
94 | + """Return whether a variable with the specified name exists in the |
95 | + expression scope.""" |
96 | + return name in self |
97 | + def value_of(name, default=None): |
98 | + """If a variable of the specified name is defined, return its value. |
99 | + Otherwise, return the provided default value, or ``None``.""" |
100 | + return self.get(name, default) |
101 | + data['defined'] = defined |
102 | + data['value_of'] = value_of |
103 | + |
104 | + |
105 | +LAZY = object() |
106 | + |
107 | + |
108 | +class DataProxy(object): |
109 | + """ |
110 | + Lazy retrieval of attributes. |
111 | + |
112 | + When a template requests data from the object being proxied by this |
113 | + object, compute the data and return it. |
114 | + """ |
115 | + def __init__(self, o): |
116 | + self._d = {} |
117 | + self._orig_d = {} # items that were members of the original object, |
118 | + # not generated later during template processing |
119 | + for k, _ in getmembers(o): |
120 | + self._d[k] = LAZY |
121 | + self._orig_d[k] = 1 |
122 | + self.obj = o |
123 | + |
124 | + def __setitem__(self, k, v): |
125 | + self._d[k] = v |
126 | + |
127 | + def __getitem__(self, k): |
128 | + if k in self._d: |
129 | + if self._d[k] is LAZY: |
130 | + v = self._d[k] = getattr(self.obj, k) |
131 | + self._d[k] = v |
132 | + else: |
133 | + v = self._d[k] |
134 | + else: |
135 | + raise KeyError(k) |
136 | + |
137 | + if callable(v) and k in self._orig_d: |
138 | + return v() |
139 | + else: |
140 | + return v |
141 | + |
142 | + def __contains__(self, k): |
143 | + return k in self._d |
144 | + |
145 | |
146 | def getGenshiString(template, data): |
147 | tmpl = MarkupTemplate(template) |
148 | - return tmpl.generate(**data).render('xhtml') |
149 | + ctx = TxGenshiContext(data) |
150 | + return tmpl.generate(ctx).render('xhtml') |
151 | + |
152 | |
153 | class genshistr(xmlstr): |
154 | |
155 | @@ -58,7 +113,7 @@ |
156 | enable your own caching mechanism and not up-call to the parent class. |
157 | """ |
158 | renderer = inevow.IRenderer(ctx) |
159 | - data = getObjectData(renderer) |
160 | + data = DataProxy(renderer) |
161 | # save a copy of the original, unmolested template, complete with |
162 | # Genshi markup |
163 | orig = self.template |
164 | @@ -72,6 +127,7 @@ |
165 | self.template = orig |
166 | return nevowRun |
167 | |
168 | + |
169 | class genshifile(xmlstr): |
170 | |
171 | def __init__(self, filename): |
172 | @@ -82,7 +138,7 @@ |
173 | self.orig = open(self._filename).read() |
174 | #self._reallyLoad(self._filename, ctx, preprocessors) |
175 | renderer = inevow.IRenderer(ctx) |
176 | - data = getObjectData(renderer) |
177 | + data = DataProxy(renderer) |
178 | self.template = getGenshiString(self.orig, data) |
179 | nevowRun = super(genshifile, self).load(ctx=ctx, |
180 | preprocessors=preprocessors) |
181 | |
182 | === modified file 'txgenshi/test/example.py' |
183 | --- txgenshi/test/example.py 2010-01-07 19:15:43 +0000 |
184 | +++ txgenshi/test/example.py 2010-01-11 01:04:10 +0000 |
185 | @@ -7,16 +7,21 @@ |
186 | |
187 | from nevow import rend |
188 | |
189 | -from txgenshi.loader import genshifile |
190 | +from txgenshi.loader import genshifile, genshistr |
191 | + |
192 | |
193 | class Apple: |
194 | color = "red" |
195 | |
196 | + |
197 | +TEMPLATEFILE = sibpath(__file__, '../../demo/templates/genshimix.xhtml') |
198 | + |
199 | + |
200 | class GenshiMixPage(rend.Page): |
201 | """ |
202 | Example page with genshi templates demo |
203 | """ |
204 | - docFactory = genshifile(sibpath(__file__, '../../demo/templates/genshimix.xhtml')) |
205 | + docFactory = genshifile(TEMPLATEFILE) |
206 | addSlash = True |
207 | |
208 | def __init__(self, username=None, *args, **kwds): |
209 | @@ -39,7 +44,7 @@ |
210 | return data.title() |
211 | |
212 | def data_username(self, ctx, data): |
213 | - return u"Joe" |
214 | + return self.username |
215 | |
216 | def date(self): |
217 | """ |
218 | @@ -49,9 +54,6 @@ |
219 | |
220 | date = property(date) |
221 | |
222 | - def render_username(self, context, data): |
223 | - """ |
224 | - It's Joe. |
225 | - """ |
226 | - return self.username |
227 | |
228 | +class GenshiMixPageStr(GenshiMixPage): |
229 | + docFactory = genshistr(open(TEMPLATEFILE).read()) |
230 | |
231 | === modified file 'txgenshi/test/test_example.py' |
232 | --- txgenshi/test/test_example.py 2010-01-07 18:57:51 +0000 |
233 | +++ txgenshi/test/test_example.py 2010-01-11 01:04:10 +0000 |
234 | @@ -17,9 +17,23 @@ |
235 | Templates generated with txGenshi work |
236 | """ |
237 | r = example.GenshiMixPage() |
238 | - s = r.renderSynchronously() |
239 | - self.assertSubstring('<p>You are Joe and your stuff is here.</p>', s) |
240 | - self.assertSubstring('<span>Your name is Joe</span>', s) |
241 | + self._testItems(r, u'Joe') |
242 | + |
243 | + def test_txGenshiExampleStr(self): |
244 | + """ |
245 | + Same as the other test, but using genshistr instead |
246 | + """ |
247 | + r = example.GenshiMixPageStr(u'Winston') |
248 | + self._testItems(r, u'Winston') |
249 | + |
250 | + def _testItems(self, resource, username): |
251 | + """ |
252 | + Verify the rendered contents of the template |
253 | + """ |
254 | + s = resource.renderSynchronously() |
255 | + self.assertSubstring('<p>You are %s and your stuff is here.</p>' % |
256 | + (username,), s) |
257 | + self.assertSubstring('<span>Your name is %s</span>' % (username,), s) |
258 | self.assertSubstring('<span>this is the value of the key</span>', s) |
259 | self.assertSubstring('Apples are red', s) |
260 | self.assertSubstring('<span>49 7 59</span>', s) |
261 | @@ -35,9 +49,12 @@ |
262 | self.assertTrue(re.search(r'<span>\s*Hello again, Dude\s*</span>', s)) |
263 | self.assertSubstring('<li class="odd">item 0</li>', s) |
264 | self.assertSubstring('<li class="even">item 9</li>', s) |
265 | - self.assertSubstring('<li>Joe</li>', s) |
266 | - self.assertTrue(re.search(r'<h2>Genshi :replace</h2>\s*Joe\s*</ul>', s)) |
267 | + self.assertSubstring('<li>%s</li>' % (username,), s) |
268 | + self.assertTrue(re.search(r'<h2>Genshi :replace</h2>\s*%s\s*</ul>' % |
269 | + (username,), s)) |
270 | self.assertTrue(re.search(r'<h2>Genshi :strip</h2>\s*<b>foo</b>\s*</div>', s)) |
271 | + self.assertSubstring('<b>Ninja Kitteh is not defined, %s</b>' % |
272 | + (username,), s) |
273 | |
274 | def test_xinclude(self): |
275 | """ |
276 | |
277 | === added file 'txgenshi/test/test_loader.py' |
278 | --- txgenshi/test/test_loader.py 1970-01-01 00:00:00 +0000 |
279 | +++ txgenshi/test/test_loader.py 2010-01-11 01:04:10 +0000 |
280 | @@ -0,0 +1,90 @@ |
281 | +""" |
282 | +Test for miscellaneous features of loader.py not covered by template |
283 | +processing specifically |
284 | +""" |
285 | +from itertools import count |
286 | + |
287 | +from twisted.trial import unittest |
288 | + |
289 | +from txgenshi import loader |
290 | + |
291 | +class TxGenshiContextTest(unittest.TestCase): |
292 | + """ |
293 | + TxGenshiContext is able to retrieve data |
294 | + """ |
295 | + def setUp(self): |
296 | + data = {'h':1, 'i': 2, 'j': 3} |
297 | + self.ctx = loader.TxGenshiContext(data) |
298 | + |
299 | + def test_access(self): |
300 | + """ |
301 | + We can do a simple get when a TGC is initialized with a dict |
302 | + """ |
303 | + self.assertEqual(self.ctx.get('h'), 1) |
304 | + self.assertEqual(self.ctx.get('k'), None) |
305 | + self.assertEqual(self.ctx.get('k', 99), 99) |
306 | + |
307 | + def test_builtins(self): |
308 | + """ |
309 | + The builtin functions defined and value_of work as advertised |
310 | + """ |
311 | + self.assertEqual(self.ctx.get('defined')('h'), True) |
312 | + self.assertEqual(self.ctx.get('value_of')('j'), 3) |
313 | + |
314 | + |
315 | +class Apple(object): |
316 | + color = 'red' |
317 | + |
318 | + def __init__(self): |
319 | + self.counter = count(27) |
320 | + self.x = 1 |
321 | + |
322 | + def eat(self): |
323 | + return "yum" |
324 | + |
325 | + @property |
326 | + def peeled(self): |
327 | + return 'peeled %s times' % (self.counter.next(),) |
328 | + |
329 | + |
330 | +class DataProxyTest(unittest.TestCase): |
331 | + """ |
332 | + A DataProxy can stand in for a dict, and correctly does caching |
333 | + """ |
334 | + def setUp(self): |
335 | + self.apple = Apple() |
336 | + self.dp = loader.DataProxy(self.apple) |
337 | + self.dp['other'] = lambda: 17.6 |
338 | + |
339 | + def test_key(self): |
340 | + """ |
341 | + We can reach normal keys |
342 | + """ |
343 | + self.assertEqual(self.dp['x'], 1) |
344 | + self.assertRaises(KeyError, lambda: self.dp['z']) |
345 | + self.assertTrue('x' in self.dp) |
346 | + |
347 | + def test_callable(self): |
348 | + """ |
349 | + Callable attributes will be called |
350 | + """ |
351 | + x = self.dp['eat'] |
352 | + self.assertEqual(x, 'yum') |
353 | + |
354 | + def test_callableAddedKeys(self): |
355 | + """ |
356 | + Test special casing for defined and value_of |
357 | + """ |
358 | + x = self.dp['other'] |
359 | + self.assertEqual(x(), 17.6) |
360 | + |
361 | + def test_caching(self): |
362 | + """ |
363 | + Values are not frozen until accessed |
364 | + """ |
365 | + # access the peel property a few times, which modifies it, to make |
366 | + # sure we are not caching the value yet |
367 | + self.apple.peeled; self.apple.peeled |
368 | + self.assertEqual(self.dp['peeled'], 'peeled 30 times') |
369 | + self.apple.peeled; self.apple.peeled |
370 | + self.assertEqual(self.dp['peeled'], 'peeled 30 times') |
Please review.
This branch uses lazy evaluation to fill in template values. I had to define my own Context subclass because of the rampant, evil use of **kwargs which prevents lazy evaluation from working, and the __init__ for that class is a copy/paste of the original Context, but the way the Genshi API is structured I didn't see any other solution.