This repository has been archived by the owner on Nov 9, 2017. It is now read-only.
/
wrapped.pyx
576 lines (488 loc) · 20.7 KB
/
wrapped.pyx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
# The contents of this file are subject to the Common Public Attribution
# License Version 1.0. (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
# software over a computer network and provide for limited attribution for the
# Original Developer. In addition, Exhibit A has been modified to be consistent
# with Exhibit B.
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
#
# The Original Code is Reddit.
#
# The Original Developer is the Initial Developer. The Initial Developer of the
# Original Code is CondeNet, Inc.
#
# All portions of the code written by CondeNet are Copyright (c) 2006-2010
# CondeNet, Inc. All Rights Reserved.
################################################################################
from itertools import chain
from datetime import datetime
import re, types
from hashlib import md5
class _TemplateUpdater(object):
# this class is just a hack to get around Cython's closure rules
__slots = ['d', 'start', 'end', 'template', 'pattern']
def __init__(self, d, start, end, template, pattern):
self.d = d
self.start, self.end = start, end
self.template = template
self.pattern = pattern
def update(self):
return self.pattern.sub(self._convert, self.template)
def _convert(self, m):
name = m.group("named")
return self.d.get(name, self.start + name + self.end)
class StringTemplate(object):
"""
Simple-minded string templating, where variables of the for $____
in a strinf are replaced with values based on a dictionary.
Unline the built-in Template class, this supports an update method
We could use the built in python Template class for this, but
unfortunately it doesn't handle unicode as gracefully as we'd
like.
"""
start_delim = "<$>"
end_delim = "</$>"
pattern2 = r"[_a-z][_a-z0-9]*"
pattern2 = r"%(start_delim)s(?:(?P<named>%(pattern)s))%(end_delim)s" % \
dict(pattern = pattern2,
start_delim = re.escape(start_delim),
end_delim = re.escape(end_delim),
)
pattern2 = re.compile(pattern2, re.UNICODE)
def __init__(self, template):
# for the nth time, we have to transform the string into
# unicode. Otherwise, re.sub will choke on non-ascii
# characters.
try:
self.template = unicode(template)
except UnicodeDecodeError:
self.template = unicode(template, "utf8")
def update(self, d):
"""
Given a dictionary of replacement rules for the Template,
replace variables in the template (once!) and return an
updated Template.
"""
if d:
updater = _TemplateUpdater(d, self.start_delim, self.end_delim,
self.template, self.pattern2)
return self.__class__(updater.update())
return self
def finalize(self, d = {}):
"""
The same as update, except the dictionary is optional and the
object returned will be a unicode object.
"""
return self.update(d).template
class CacheStub(object):
"""
When using cached renderings, this class generates a stub based on
the hash of the Templated item passed into init for the style
specified.
This class is suitable as a stub object (in the case of API calls)
and wil render in a string form suitable for replacement with
StringTemplate in the case of normal rendering.
"""
def __init__(self, item, style):
self.name = "h%s%s" % (id(item), str(style).replace('-', '_'))
def __str__(self):
return StringTemplate.start_delim + self.name + \
StringTemplate.end_delim
def __repr__(self):
return "<%s: %s>" % (self.__class__.__name__, self.name)
class CachedVariable(CacheStub):
"""
Same use as CacheStubs in normal templates, except it can be
applied to where we would normally put a '$____' variable by hand
in a template (file).
"""
def __init__(self, name):
self.name = name
class Templated(object):
"""
Replaces the Wrapped class (which has now a subclass and which
takes an thing to be wrapped).
Templated objects are suitable for rendering and caching, with a
render loop desgined to fetch other cached templates and insert
them into the current template.
"""
# is this template cachable (see CachedTemplate)
cachable = False
# attributes that will not be made into the cache key
cache_ignore = set()
def __repr__(self):
return "<Templated: %s>" % self.__class__.__name__
def __init__(self, **context):
"""
uses context to init __dict__ (making this object a bit like a storage)
"""
for k, v in context.iteritems():
setattr(self, k, v)
if not hasattr(self, "render_class"):
self.render_class = self.__class__
def _notfound(self, style):
from pylons import g, request
from pylons.controllers.util import abort
from r2.lib.log import log_text
if g.debug:
raise NotImplementedError (repr(self), style)
else:
if style == 'png':
level = "debug"
else:
level = "warning"
log_text("missing template",
"Couldn't find %s template for %r %s" %
(style, self, request.path),
level)
abort(404)
def template(self, style = 'html'):
"""
Fetches template from the template manager
"""
from r2.config.templates import tpm
from pylons import g
use_cache = not g.reload_templates
template = None
try:
template = tpm.get(self.render_class,
style, cache = use_cache)
except AttributeError:
self._notfound(style)
return template
def cache_key(self, *a):
"""
if cachable, this function is used to generate the cache key.
"""
raise NotImplementedError
def render_nocache(self, attr, style):
"""
No-frills (or caching) rendering of the template. The
template is returned as a subclass of StringTemplate and
therefore finalize() must be called on it to turn it into its
final form
"""
from filters import unsafe
from pylons import c
# the style has to default to the global render style
# fetch template
template = self.template(style)
if template:
# store the global render style (since child templates)
render_style = c.render_style
c.render_style = style
# are we doing a partial render?
if attr:
template = template.get_def(attr)
# render the template
res = template.render(thing = self)
if not isinstance(res, StringTemplate):
res = StringTemplate(res)
# reset the global render style
c.render_style = render_style
return res
else:
self._notfound(style)
def _render(self, attr, style, **kwargs):
"""
Renders the current template with the current style, possibly
doing a part_render if attr is not None.
if this is the first template to be rendered, it is will track
cachable templates, insert stubs for them in the output,
get_multi from the cache, and render the uncached templates.
Uncached but cachable templates are inserted back into the
cache with a set_multi.
NOTE: one of the interesting issues with this function is that
on each newly rendered thing, it is possible that that
rendering has in turn cause more cachable things to be
fetched. Thus the first template to be rendered runs a loop
and keeps rendering until there is nothing left to render.
Then it updates the master template until it doesn't change.
NOTE 2: anything passed in as a kw to render (and thus
_render) will not be part of the cached version of the object,
and will substituted last.
"""
from pylons import c, g
style = style or c.render_style or 'html'
# prepare (and store) the list of cachable items.
primary = False
if not isinstance(c.render_tracker, dict):
primary = True
c.render_tracker = {}
# insert a stub for cachable non-primary templates
if self.cachable:
res = CacheStub(self, style)
cache_key = self.cache_key(attr, style)
# in the tracker, we need to store:
# The render cache key (res.name)
# The memcached cache key(cache_key)
# who I am (self) and what am I doing (attr, style) with what
# (kwargs)
c.render_tracker[res.name] = (cache_key, (self,
(attr, style, kwargs)))
else:
# either a primary template or not cachable, so render it
res = self.render_nocache(attr, style)
# if this is the primary template, let the caching games begin
if primary:
# updates will be the (self-updated) list of all of
# the cached templates that have been cached or
# rendered.
updates = {}
# to_cache is just the keys of the cached templates
# that were not in the cache.
to_cache = set([])
while c.render_tracker:
# copy and wipe the tracker. It'll get repopulated if
# any of the subsequent render()s call cached objects.
current = c.render_tracker
c.render_tracker = {}
# do a multi-get. NOTE: cache keys are the first item
# in the tuple that is the current dict's values.
# This dict cast will generate a new dict of cache_key
# to value
cached = self._read_cache(dict(current.values()))
# replacements will be a map of key -> rendered content
# for updateing the current set of updates
replacements = {}
new_updates = {}
# render items that didn't make it into the cached list
for key, (cache_key, others) in current.iteritems():
# unbundle the remaining args
item, (attr, style, kw) = others
if cache_key not in cached:
# this had to be rendered, so cache it later
to_cache.add(cache_key)
# render the item and apply the stored kw args
r = item.render_nocache(attr, style)
else:
r = cached[cache_key]
# store the unevaluated templates in
# cached for caching
replacements[key] = r.finalize(kw)
new_updates[key] = (cache_key, (r, kw))
# update the updates so that when we can do the
# replacement in one pass.
# NOTE: keep kw, but don't update based on them.
# We might have to cache these later, and we want
# to have things like $child present.
for k in updates.keys():
cache_key, (value, kw) = updates[k]
value = value.update(replacements)
updates[k] = cache_key, (value, kw)
updates.update(new_updates)
# at this point, we haven't touched res, but updates now
# has the list of all the updates we could conceivably
# want to make, and to_cache is the list of cache keys
# that we didn't find in the cache.
# cache content that was newly rendered
_to_cache = {}
for k, (v, kw) in updates.values():
if k in to_cache:
_to_cache[k] = v
self._write_cache(_to_cache)
# edge case: this may be the primary tempalte and cachable
if isinstance(res, CacheStub):
res = updates[res.name][1][0]
# now we can update the updates to make use of their kw args.
_updates = {}
for k, (foo, (v, kw)) in updates.iteritems():
_updates[k] = v.finalize(kw)
updates = _updates
# update the response to use these values
# replace till we can't replace any more.
npasses = 0
while True:
npasses += 1
r = res
res = res.update(kwargs).update(updates)
semi_final = res.finalize()
if r.finalize() == res.finalize():
res = semi_final
break
# wipe out the render tracker object
c.render_tracker = None
elif not isinstance(res, CacheStub):
# we're done. Update the template based on the args passed in
res = res.finalize(kwargs)
return res
def _cache_key(self, key):
return 'render_%s(%s)' % (self.__class__.__name__,
md5(key).hexdigest())
def _write_cache(self, keys):
from pylons import g
if not keys:
return
toset = {}
for key, val in keys.iteritems():
toset[self._cache_key(key)] = val
g.rendercache.set_multi(toset)
def _read_cache(self, keys):
from pylons import g
ekeys = {}
for key in keys:
ekeys[self._cache_key(key)] = key
found = g.rendercache.get_multi(ekeys)
ret = {}
for fkey, val in found.iteritems():
ret[ekeys[fkey]] = val
return ret
def render(self, style = None, **kw):
from r2.lib.filters import unsafe
res = self._render(None, style, **kw)
return unsafe(res) if isinstance(res, str) else res
def part_render(self, attr, **kw):
style = kw.get('style')
if style: del kw['style']
return self._render(attr, style, **kw)
class Uncachable(Exception): pass
_easy_cache_cls = set([bool, int, long, float, unicode, str, types.NoneType,
datetime])
def make_cachable(v, *a):
"""
Given an arbitrary object,
"""
if v.__class__ in _easy_cache_cls or isinstance(v, type):
try:
return unicode(v)
except UnicodeDecodeError:
try:
return unicode(v, "utf8")
except (TypeError, UnicodeDecodeError):
return repr(v)
elif isinstance(v, (types.MethodType, CachedVariable) ):
return
elif isinstance(v, (tuple, list, set)):
return repr([make_cachable(x, *a) for x in v])
elif isinstance(v, dict):
ret = {}
for k in sorted(v.iterkeys()):
ret[k] = make_cachable(v[k], *a)
return repr(ret)
elif hasattr(v, "cache_key"):
return v.cache_key(*a)
else:
raise Uncachable, "%s, %s" % (v, type(v))
class CachedTemplate(Templated):
cachable = True
def cachable_attrs(self):
"""
Generates an iterator of attr names and their values for every
attr on this element that should be used in generating the cache key.
"""
ret = []
for k in sorted(self.__dict__):
if k not in self.cache_ignore and not k.startswith('_'):
ret.append((k, self.__dict__[k]))
return ret
def cache_key(self, attr, style, *a):
from pylons import c
# if template debugging is on, there will be no hash and we
# can make the caching process-local.
template_hash = getattr(self.template(style), "hash",
id(self.__class__))
# these values are needed to render any link on the site, and
# a menu is just a set of links, so we best cache against
# them.
keys = [c.user_is_loggedin, c.user_is_admin, c.domain_prefix,
style, c.secure, c.cname, c.lang, c.site.path,
getattr(c.user, "gold", False),
template_hash]
# if viewing a single subreddit, take flair settings into account.
if c.user and hasattr(c.site, '_id'):
keys.extend([
c.site.flair_enabled, c.site.flair_position,
c.site.link_flair_position,
c.user.flair_enabled_in_sr(c.site._id),
c.user.pref_show_flair])
keys = [make_cachable(x, *a) for x in keys]
# add all parameters sent into __init__, using their current value
auto_keys = [(k, make_cachable(v, attr, style, *a))
for k, v in self.cachable_attrs()]
# lastly, add anything else that was passed in.
keys.append(repr(auto_keys))
for x in a:
keys.append(make_cachable(x))
return "<%s:[%s]>" % (self.__class__.__name__, u''.join(keys))
class Wrapped(CachedTemplate):
# default to false, evaluate
cachable = False
cache_ignore = set(['lookups'])
def cache_key(self, attr, style):
if self.cachable:
for i, l in enumerate(self.lookups):
if hasattr(l, "wrapped_cache_key"):
# setattr will force a __dict__ entry, but only if the
# param doesn't start with "_"
setattr(self, "lookup%d_cache_key" % i,
''.join(map(repr,
l.wrapped_cache_key(self, style))))
return CachedTemplate.cache_key(self, attr, style)
def __init__(self, *lookups, **context):
self.lookups = lookups
# set the default render class to be based on the lookup
if self.__class__ == Wrapped and lookups:
self.render_class = lookups[0].__class__
else:
self.render_class = self.__class__
# this shouldn't be too surprising
self.cache_ignore = self.cache_ignore.union(
set(['cachable', 'render', 'cache_ignore', 'lookups']))
if (not self._any_hasattr(lookups, 'cachable') and
self._any_hasattr(lookups, 'wrapped_cache_key')):
self.cachable = True
if self.cachable:
for l in lookups:
if hasattr(l, "cache_ignore"):
self.cache_ignore = self.cache_ignore.union(l.cache_ignore)
Templated.__init__(self, **context)
def _any_hasattr(self, lookups, attr):
for l in lookups:
if hasattr(l, attr):
return True
def __repr__(self):
return "<Wrapped: %s, %s>" % (self.__class__.__name__,
self.lookups)
def __getattr__(self, attr):
if attr == 'lookups':
raise AttributeError, attr
res = None
found = False
for lookup in self.lookups:
try:
res = getattr(lookup, attr)
found = True
break
except AttributeError:
pass
if not found:
raise AttributeError, "%r has no %s" % (self, attr)
setattr(self, attr, res)
return res
def __iter__(self):
if self.lookups and hasattr(self.lookups[0], "__iter__"):
return self.lookups[0].__iter__()
raise NotImplementedError
class Styled(CachedTemplate):
"""Rather than creating a separate template for every possible
menu/button style we might want to use, this class overrides the
render function to render only the <%def> in the template whose
name matches 'style'.
Additionally, when rendering, the '_id' and 'css_class' attributes
are intended to be used in the outermost container's id and class
tag.
"""
def __init__(self, style, _id = '', css_class = '', **kw):
self._id = _id
self.css_class = css_class
self.style = style
CachedTemplate.__init__(self, **kw)
def render(self, **kw):
"""Using the canonical template file, only renders the <%def>
in the template whose name is given by self.style"""
return CachedTemplate.part_render(self, self.style, **kw)