Merge lp:~corydodt/txgenshi/502823 into lp:txgenshi

Proposed by Cory Dodt
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
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.

To post a comment you must log in.
Revision history for this message
Cory Dodt (corydodt) wrote : Posted in a previous version of this proposal

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.

Revision history for this message
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.

review: Approve
Revision history for this message
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://www.ohloh.net/p/txgenshi/contributors/2060925024795272

Revision history for this message
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://www.ohloh.net/p/txgenshi/contributors/2060925024795272
> --
> https://code.edge.launchpad.net/~corydodt/txgenshi/502823/+merge/17116
> You are the owner of lp:~corydodt/txgenshi/502823.
>

--
_____________________

Revision history for this message
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://www.ohloh.net/p/txgenshi/contributors/2060925024795272
> > --
> > https://code.edge.launchpad.net/~corydodt/txgenshi/502823/+merge/17116
> > You are the owner of lp:~corydodt/txgenshi/502823.
> >
>
>
>
> --
> _____________________
>
> https://code.launchpad.net/~corydodt/txgenshi/502823/+merge/17116
> You are the owner of lp:~corydodt/txgenshi/502823.
>

--
_____________________

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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')

Subscribers

People subscribed via source and target branches

to all changes: