Coverage for tw2/core/resources.py : 95%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import absolute_import
3import re
4import logging
5import itertools
6import os
7import webob as wo
8import pkg_resources as pr
9import mimetypes
10import inspect
11import warnings
12import wsgiref.util
14from .widgets import Widget
15from .util import MultipleReplacer
16import tw2.core.core
17from .params import Param, Variable, ParameterError, Required
18from .middleware import register_resource
19from .js import encoder, js_symbol
21from markupsafe import Markup
22import six
24log = logging.getLogger(__name__)
27# TBD is there a better place to put this?
28mimetypes.init()
29mimetypes.types_map['.ico'] = 'image/x-icon'
32class JSSymbol(js_symbol):
33 """ Deprecated compatibility shim with old TW2 stuff. Use js_symbol. """
35 def __init__(self, *args, **kw):
36 warnings.warn("JSSymbol is deprecated. Please use js_symbol")
38 if len(args) > 1:
39 raise ValueError("JSSymbol must receive up to only one arg.")
41 if len(args) == 1 and 'src' in kw:
42 raise ValueError("JSSymbol must receive only one src arg.")
44 if len(args) == 1:
45 kw['src'] = args[0]
47 super(JSSymbol, self).__init__(**kw)
49 # Backwards compatibility for accessing the source.
50 self.src = self._name
53class ResourceBundle(Widget):
54 """ Just a list of resources.
56 Use it as follows:
58 >>> jquery_ui = ResourceBundle(resources=[jquery_js, jquery_css])
59 >>> jquery_ui.inject()
61 """
63 @classmethod
64 def inject(cls):
65 cls.req().prepare()
67 def prepare(self):
68 super(ResourceBundle, self).prepare()
70 rl = tw2.core.core.request_local()
71 rl_resources = rl.setdefault('resources', [])
72 rl_location = rl['middleware'].config.inject_resources_location
74 if self not in rl_resources:
75 for r in self.resources:
76 r.req().prepare()
79class Resource(ResourceBundle):
80 """A resource required by a widget being displayed.
82 ``location`` states where the resource should be injected
83 into the page. Can be any of ``head``, ``headbottom``,
84 ``bodytop`` or ``bodybottom`` or ``None``.
85 """
86 location = Param(
87 'Location on the page where the resource should be placed.' \
88 'This can be one of: head, headbottom, bodytop or bodybottom. '\
89 'None means the resource will not be injected, which is still '\
90 'useful, e.g. static images.', default=None)
91 id = None
92 template = None
94 def prepare(self):
95 super(Resource, self).prepare()
97 rl = tw2.core.core.request_local()
98 rl_resources = rl.setdefault('resources', [])
99 rl_location = rl['middleware'].config.inject_resources_location
101 if self not in rl_resources:
102 if self.location is '__use_middleware':
103 self.location = rl_location
105 rl_resources.append(self)
108class Link(Resource):
109 '''
110 A link to a file.
112 The ``link`` parameter can be used to specify the explicit
113 link to a URL.
115 If omitted, the link will be built to serve ``filename``
116 from ``modname`` as a resource coming from a python
117 distribution.
118 '''
119 id = None
120 link = Param(
121 'Direct web link to file. If this is not specified, it is ' +
122 'automatically generated, based on :attr:`modname` and ' +
123 ':attr:`filename`.',
124 )
125 modname = Param(
126 'Name of Python module that contains the file.',
127 default=None,
128 )
129 filename = Param(
130 'Path to file, relative to module base.',
131 default=None,
132 )
133 no_inject = Param(
134 "Don't inject this link. (Default: False)",
135 default=False,
136 )
137 whole_dir = Param(
138 "Make the whole directory available. (Default: False)",
139 default=False,
140 )
142 @classmethod
143 def guess_modname(cls):
144 """ Try to guess my modname.
146 If I wasn't supplied any modname, take a guess by stepping back up the
147 frame stack until I find something not in tw2.core
148 """
150 try:
151 frame, i = inspect.stack()[0][0], 0
152 while frame.f_globals['__name__'].startswith('tw2.core'):
153 frame, i = inspect.stack()[i][0], i + 1
155 return frame.f_globals['__name__']
156 except Exception:
157 return None
159 @classmethod
160 def post_define(cls):
162 if not cls.no_inject:
163 if getattr(cls, 'filename', None) and \
164 type(cls.filename) != property:
166 if not cls.modname:
167 cls.modname = cls.guess_modname()
169 register_resource(
170 cls.modname or '__anon__', cls.filename, cls.whole_dir
171 )
173 def prepare(self):
174 rl = tw2.core.core.request_local()
175 if not self.no_inject:
176 if not hasattr(self, 'link'):
177 # TBD shouldn't we test for this in __new__ ?
178 if not self.filename:
179 raise ParameterError(
180 "Either 'link' or 'filename' must be specified"
181 )
182 resources = rl['middleware'].resources
183 self.link = resources.resource_path(
184 self.modname or '__anon__', self.filename
185 )
186 super(Link, self).prepare()
188 def __hash__(self):
189 return hash(
190 hasattr(self, 'link') and \
191 self.link or \
192 ((self.modname or '') + self.filename)
193 )
195 def __eq__(self, other):
196 return (isinstance(other, Link) and self.link == other.link
197 and self.modname == other.modname
198 and self.filename == other.filename)
200 def __repr__(self):
201 return "%s('%s')" % (
202 self.__class__.__name__,
203 getattr(self, 'link', '%s/%s' % (self.modname, self.filename))
204 )
207class DirLink(Link):
208 ''' A whole directory as a resource.
210 Unlike :class:`JSLink` and :class:`CSSLink`, this resource doesn't inject
211 anything on the page.. but it does register all resources under the
212 marked directory to be served by the middleware.
214 This is useful if you have a css file that pulls in a number of other
215 static resources like icons and images.
216 '''
217 link = Variable()
218 filename = Required
219 whole_dir = True
221 def prepare(self):
222 resources = tw2.core.core.request_local()['middleware'].resources
223 self.link = resources.resource_path(
224 self.modname,
225 self.filename,
226 )
229class JSLink(Link):
230 '''
231 A JavaScript source file.
233 By default is injected in whatever default place
234 is specified by the middleware.
235 '''
236 location = '__use_middleware'
237 template = 'tw2.core.templates.jslink'
240class CSSLink(Link):
241 '''
242 A CSS style sheet.
244 By default it's injected at the top of the head node.
245 '''
246 media = Param('Media tag', default='all')
247 location = 'head'
248 template = 'tw2.core.templates.csslink'
251class JSSource(Resource):
252 """
253 Inline JavaScript source code.
255 By default is injected before the </body> is closed
256 """
257 src = Param('Source code', default=None)
258 location = 'bodybottom'
259 template = 'tw2.core.templates.jssource'
261 def __eq__(self, other):
262 return isinstance(other, JSSource) and self.src == other.src
264 def __repr__(self):
265 return "%s('%s')" % (self.__class__.__name__, self.src)
267 def prepare(self):
268 super(JSSource, self).prepare()
269 if not self.src:
270 raise ValueError("%r must be provided a 'src' attr" % self)
271 self.src = Markup(self.src)
274class CSSSource(Resource):
275 """
276 Inline Cascading Style-Sheet code.
278 By default it's injected at the top of the head node.
279 """
280 src = Param('CSS code', default=None)
281 location = 'head'
282 template = 'tw2.core.templates.csssource'
284 def __eq__(self, other):
285 return isinstance(other, CSSSource) and self.src == other.src
287 def __repr__(self):
288 return "%s('%s')" % (self.__class__.__name__, self.src)
290 def prepare(self):
291 super(CSSSource, self).prepare()
292 if not self.src:
293 raise ValueError("%r must be provided a 'src' attr" % self)
294 self.src = Markup(self.src)
297class _JSFuncCall(JSSource):
298 """
299 Internal use inline JavaScript function call.
301 Please use tw2.core.js_function(...) externally.
302 """
303 src = None
304 function = Param('Function name', default=None)
305 args = Param('Function arguments', default=None)
306 location = 'bodybottom' # TBD: afterwidget?
308 def __str__(self):
309 if not self.src:
310 self.prepare()
311 return self.src
313 def prepare(self):
314 if not self.src:
315 args = ''
316 if isinstance(self.args, dict):
317 args = encoder.encode(self.args)
318 elif self.args:
319 args = ', '.join(encoder.encode(a) for a in self.args)
321 self.src = '%s(%s)' % (self.function, args)
322 super(_JSFuncCall, self).prepare()
324 def __hash__(self):
325 if self.args:
326 if isinstance(self.args, dict):
327 sargs = encoder.encode(self.args)
328 else:
329 sargs = ', '.join(encoder.encode(a) for a in self.args)
330 else:
331 sargs = None
333 return hash((hasattr(self, 'src') and self.src or '') + (sargs or ''))
335 def __eq__(self, other):
336 return (getattr(self, 'src', None) == getattr(other, 'src', None)
337 and getattr(self, 'args', None) == getattr(other, 'args', None)
338 )
341class ResourcesApp(object):
342 """WSGI Middleware to serve static resources
344 This handles URLs like this:
345 /resources/tw2.forms/static/forms.css
347 Where:
348 resources is the prefix
349 tw2.forms is a python package name
350 static is a directory inside the package
351 forms.css is the file to retrieve
353 For this to work, the file must have been registered in advance,
354 using :meth:`register`. There is a ResourcesApp instance for each
355 TwMiddleware instance.
356 """
358 def __init__(self, config):
359 self._paths = {}
360 self._dirs = []
361 self.config = config
363 def register(self, modname, filename, whole_dir=False):
364 """ Register a file for static serving.
366 After this method has been called, for say ('tw2.forms',
367 'static/forms.css'), the URL /resources/tw2.forms/static/forms.css will
368 then serve that file from within the tw2.forms package. This works
369 correctly for zipped eggs.
371 *Security Consideration* - This file will be readable by users of the
372 application, so make sure it contains no confidential data. For
373 DirLink resources, the whole directory, and subdirectories will be
374 readable.
376 `modname`
377 The python module that contains the file to publish. You can also
378 pass a pkg_resources.Requirement instance to point to the root of
379 an egg distribution.
381 `filename`
382 The path, relative to the base of the module, of the file to be
383 published. If *modname* is None, it's an absolute path.
384 """
385 if isinstance(modname, pr.Requirement):
386 modname = os.path.basename(pr.working_set.find(modname).location)
388 path = modname + '/' + filename.lstrip('/')
390 if whole_dir:
391 if path not in self._dirs:
392 self._dirs.append(path)
393 else:
394 if path not in self._paths:
395 self._paths[path] = (modname, filename)
397 def resource_path(self, modname, filename):
398 """ Return a resource's web path. """
400 if isinstance(modname, pr.Requirement):
401 modname = os.path.basename(pr.working_set.find(modname).location)
403 path = modname + '/' + filename.lstrip('/')
404 return self.config.script_name + self.config.res_prefix + path
406 def __call__(self, environ, start_response):
407 req = wo.Request(environ)
408 try:
409 path = environ['PATH_INFO']
410 path = path[len(self.config.res_prefix):]
412 if path not in self._paths:
413 if '..' in path: # protect against directory traversal
414 raise IOError()
415 for d in self._dirs:
416 if path.startswith(d.replace('\\', '/')):
417 break
418 else:
419 raise IOError()
420 modname, filename = path.lstrip('/').split('/', 1)
421 ct, enc = mimetypes.guess_type(os.path.basename(filename))
422 if modname and modname != '__anon__':
423 stream = pr.resource_stream(modname, filename)
424 else:
425 stream = open(filename)
426 except IOError:
427 resp = wo.Response(status="404 Not Found")
428 else:
429 stream = wsgiref.util.FileWrapper(stream, self.config.bufsize)
430 resp = wo.Response(app_iter=stream, content_type=ct)
431 if enc:
432 resp.content_type_params['charset'] = enc
433 resp.cache_control = {'max-age': int(self.config.res_max_age)}
434 return resp(environ, start_response)
437class _ResourceInjector(MultipleReplacer):
438 """
439 ToscaWidgets can inject resources that have been registered for injection
440 in the current request.
442 Usually widgets register them when they're displayed and they have
443 instances of :class:`tw2.core.resources.Resource` declared at their
444 :attr:`tw2.core.Widget.javascript` or :attr:`tw2.core.Widget.css`
445 attributes.
447 Resources can also be registered manually from a controller or template by
448 calling their :meth:`tw2.core.resources.Resource.inject` method.
450 When a page including widgets is rendered, Resources that are registered
451 for injection are collected in a request-local storage area (this means
452 any thing stored here is only visible to one single thread of execution
453 and that its contents are freed when the request is finished) where they
454 can be rendered and injected in the resulting html.
456 ToscaWidgets' middleware can take care of injecting them automatically
457 (default) but they can also be injected explicitly, example::
459 >>> from tw2.core.resources import JSLink, inject_resources
460 >>> JSLink(link="http://example.com").inject()
461 >>> html = "<html><head></head><body></body></html>"
462 >>> inject_resources(html)
463 '<html><head><script type="text/javascript"
464 src="http://example.com"></script></head><body></body></html>'
466 Once resources have been injected they are popped from request local and
467 cannot be injected again (in the same request). This is useful in case
468 :class:`injector_middleware` is stacked so it doesn't inject them again.
470 Injecting them explicitly is necessary if the response's body is being
471 cached before the middleware has a chance to inject them because when the
472 cached version is served no widgets are being rendered so they will not
473 have a chance to register their resources.
474 """
476 def __init__(self):
477 return MultipleReplacer.__init__(self, {
478 r'<head(?!er).*?>': self._injector_for_location('head'),
479 r'</head(?!er).*?>': self._injector_for_location(
480 'headbottom', False
481 ),
482 r'<body.*?>': self._injector_for_location('bodytop'),
483 r'</body.*?>': self._injector_for_location('bodybottom', False)
484 }, re.I | re.M)
486 def _injector_for_location(self, key, after=True):
487 def inject(group, resources, encoding):
488 inj = six.u('\n').join([
489 r.display(displays_on='string')
490 for r in resources
491 if r.location == key
492 ])
493 if after:
494 return group + inj
495 return inj + group
496 return inject
498 def __call__(self, html, resources=None, encoding=None):
499 """Injects resources, if any, into html string when called.
501 .. note::
502 Ignore the ``self`` parameter if seeing this as
503 :func:`tw.core.resource_injector.inject_resources` docstring
504 since it is an alias for an instance method of a private class.
506 ``html`` must be a ``encoding`` encoded string. If ``encoding`` is not
507 given it will be tried to be derived from a <meta>.
509 """
510 if resources is None:
511 resources = tw2.core.core.request_local().get('resources', None)
512 if resources:
513 encoding = encoding or find_charset(html) or 'utf-8'
514 html = MultipleReplacer.__call__(
515 self, html, resources, encoding
516 )
517 tw2.core.core.request_local().pop('resources', None)
518 return html
521# Bind __call__ directly so docstring is included in docs
522inject_resources = _ResourceInjector().__call__
525_charset_re = re.compile(
526 r"charset\s*=\s*(?P<charset>[\w-]+)([^\>])*", re.I | re.M)
529def find_charset(string):
530 m = _charset_re.search(string)
531 if m:
532 return m.group('charset').lower()