PageRenderTime 43ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

/r2/r2/lib/wrapped.pyx

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