Source code for tw2.core.resources

from __future__ import absolute_import

import re
import logging
import itertools
import os
import webob as wo
import pkg_resources as pr
import mimetypes
import inspect
import warnings
import wsgiref.util

from .widgets import Widget
from .util import MultipleReplacer
import tw2.core.core
from .params import Param, Variable, ParameterError, Required
from .middleware import register_resource
from .js import encoder, js_symbol

from markupsafe import Markup
import six

log = logging.getLogger(__name__)


# TBD is there a better place to put this?
mimetypes.init()
mimetypes.types_map['.ico'] = 'image/x-icon'


class JSSymbol(js_symbol):
    """ Deprecated compatibility shim with old TW2 stuff.  Use js_symbol. """

    def __init__(self, *args, **kw):
        warnings.warn("JSSymbol is deprecated.  Please use js_symbol")

        if len(args) > 1:
            raise ValueError("JSSymbol must receive up to only one arg.")

        if len(args) == 1 and 'src' in kw:
            raise ValueError("JSSymbol must receive only one src arg.")

        if len(args) == 1:
            kw['src'] = args[0]

        super(JSSymbol, self).__init__(**kw)

        # Backwards compatibility for accessing the source.
        self.src = self._name


[docs]class ResourceBundle(Widget): """ Just a list of resources. Use it as follows: >>> jquery_ui = ResourceBundle(resources=[jquery_js, jquery_css]) >>> jquery_ui.inject() """ @classmethod def inject(cls): cls.req().prepare() def prepare(self): super(ResourceBundle, self).prepare() rl = tw2.core.core.request_local() rl_resources = rl.setdefault('resources', []) rl_location = rl['middleware'].config.inject_resources_location if self not in rl_resources: for r in self.resources: r.req().prepare()
[docs]class Resource(ResourceBundle): """A resource required by a widget being displayed. ``location`` states where the resource should be injected into the page. Can be any of ``head``, ``headbottom``, ``bodytop`` or ``bodybottom`` or ``None``. """ location = Param( 'Location on the page where the resource should be placed.' \ 'This can be one of: head, headbottom, bodytop or bodybottom. '\ 'None means the resource will not be injected, which is still '\ 'useful, e.g. static images.', default=None) id = None template = None def prepare(self): super(Resource, self).prepare() rl = tw2.core.core.request_local() rl_resources = rl.setdefault('resources', []) rl_location = rl['middleware'].config.inject_resources_location if self not in rl_resources: if self.location is '__use_middleware': self.location = rl_location rl_resources.append(self)
[docs]class JSSource(Resource): """ Inline JavaScript source code. By default is injected before the </body> is closed """ src = Param('Source code', default=None) location = 'bodybottom' template = 'tw2.core.templates.jssource' def __eq__(self, other): return isinstance(other, JSSource) and self.src == other.src def __repr__(self): return "%s('%s')" % (self.__class__.__name__, self.src) def prepare(self): super(JSSource, self).prepare() if not self.src: raise ValueError("%r must be provided a 'src' attr" % self) self.src = Markup(self.src)
[docs]class CSSSource(Resource): """ Inline Cascading Style-Sheet code. By default it's injected at the top of the head node. """ src = Param('CSS code', default=None) location = 'head' template = 'tw2.core.templates.csssource' def __eq__(self, other): return isinstance(other, CSSSource) and self.src == other.src def __repr__(self): return "%s('%s')" % (self.__class__.__name__, self.src) def prepare(self): super(CSSSource, self).prepare() if not self.src: raise ValueError("%r must be provided a 'src' attr" % self) self.src = Markup(self.src)
class _JSFuncCall(JSSource): """ Internal use inline JavaScript function call. Please use tw2.core.js_function(...) externally. """ src = None function = Param('Function name', default=None) args = Param('Function arguments', default=None) location = 'bodybottom' # TBD: afterwidget? def __str__(self): if not self.src: self.prepare() return self.src def prepare(self): if not self.src: args = '' if isinstance(self.args, dict): args = encoder.encode(self.args) elif self.args: args = ', '.join(encoder.encode(a) for a in self.args) self.src = '%s(%s)' % (self.function, args) super(_JSFuncCall, self).prepare() def __hash__(self): if self.args: if isinstance(self.args, dict): sargs = encoder.encode(self.args) else: sargs = ', '.join(encoder.encode(a) for a in self.args) else: sargs = None return hash((hasattr(self, 'src') and self.src or '') + (sargs or '')) def __eq__(self, other): return (getattr(self, 'src', None) == getattr(other, 'src', None) and getattr(self, 'args', None) == getattr(other, 'args', None) ) class ResourcesApp(object): """WSGI Middleware to serve static resources This handles URLs like this: /resources/tw2.forms/static/forms.css Where: resources is the prefix tw2.forms is a python package name static is a directory inside the package forms.css is the file to retrieve For this to work, the file must have been registered in advance, using :meth:`register`. There is a ResourcesApp instance for each TwMiddleware instance. """ def __init__(self, config): self._paths = {} self._dirs = [] self.config = config def register(self, modname, filename, whole_dir=False): """ Register a file for static serving. After this method has been called, for say ('tw2.forms', 'static/forms.css'), the URL /resources/tw2.forms/static/forms.css will then serve that file from within the tw2.forms package. This works correctly for zipped eggs. *Security Consideration* - This file will be readable by users of the application, so make sure it contains no confidential data. For DirLink resources, the whole directory, and subdirectories will be readable. `modname` The python module that contains the file to publish. You can also pass a pkg_resources.Requirement instance to point to the root of an egg distribution. `filename` The path, relative to the base of the module, of the file to be published. If *modname* is None, it's an absolute path. """ if isinstance(modname, pr.Requirement): modname = os.path.basename(pr.working_set.find(modname).location) path = modname + '/' + filename.lstrip('/') if whole_dir: if path not in self._dirs: self._dirs.append(path) else: if path not in self._paths: self._paths[path] = (modname, filename) def resource_path(self, modname, filename): """ Return a resource's web path. """ if isinstance(modname, pr.Requirement): modname = os.path.basename(pr.working_set.find(modname).location) path = modname + '/' + filename.lstrip('/') return self.config.script_name + self.config.res_prefix + path def __call__(self, environ, start_response): req = wo.Request(environ) try: path = environ['PATH_INFO'] path = path[len(self.config.res_prefix):] if path not in self._paths: if '..' in path: # protect against directory traversal raise IOError() for d in self._dirs: if path.startswith(d.replace('\\', '/')): break else: raise IOError() modname, filename = path.lstrip('/').split('/', 1) ct, enc = mimetypes.guess_type(os.path.basename(filename)) if modname and modname != '__anon__': stream = pr.resource_stream(modname, filename) else: stream = open(filename) except IOError: resp = wo.Response(status="404 Not Found") else: stream = wsgiref.util.FileWrapper(stream, self.config.bufsize) resp = wo.Response(app_iter=stream, content_type=ct) if enc: resp.content_type_params['charset'] = enc resp.cache_control = {'max-age': int(self.config.res_max_age)} return resp(environ, start_response) class _ResourceInjector(MultipleReplacer): """ ToscaWidgets can inject resources that have been registered for injection in the current request. Usually widgets register them when they're displayed and they have instances of :class:`tw2.core.resources.Resource` declared at their :attr:`tw2.core.Widget.javascript` or :attr:`tw2.core.Widget.css` attributes. Resources can also be registered manually from a controller or template by calling their :meth:`tw2.core.resources.Resource.inject` method. When a page including widgets is rendered, Resources that are registered for injection are collected in a request-local storage area (this means any thing stored here is only visible to one single thread of execution and that its contents are freed when the request is finished) where they can be rendered and injected in the resulting html. ToscaWidgets' middleware can take care of injecting them automatically (default) but they can also be injected explicitly, example:: >>> from tw2.core.resources import JSLink, inject_resources >>> JSLink(link="http://example.com").inject() >>> html = "<html><head></head><body></body></html>" >>> inject_resources(html) '<html><head><script type="text/javascript" src="http://example.com"></script></head><body></body></html>' Once resources have been injected they are popped from request local and cannot be injected again (in the same request). This is useful in case :class:`injector_middleware` is stacked so it doesn't inject them again. Injecting them explicitly is necessary if the response's body is being cached before the middleware has a chance to inject them because when the cached version is served no widgets are being rendered so they will not have a chance to register their resources. """ def __init__(self): return MultipleReplacer.__init__(self, { r'<head(?!er).*?>': self._injector_for_location('head'), r'</head(?!er).*?>': self._injector_for_location( 'headbottom', False ), r'<body.*?>': self._injector_for_location('bodytop'), r'</body.*?>': self._injector_for_location('bodybottom', False) }, re.I | re.M) def _injector_for_location(self, key, after=True): def inject(group, resources, encoding): inj = six.u('\n').join([ r.display(displays_on='string') for r in resources if r.location == key ]) if after: return group + inj return inj + group return inject def __call__(self, html, resources=None, encoding=None): """Injects resources, if any, into html string when called. .. note:: Ignore the ``self`` parameter if seeing this as :func:`tw.core.resource_injector.inject_resources` docstring since it is an alias for an instance method of a private class. ``html`` must be a ``encoding`` encoded string. If ``encoding`` is not given it will be tried to be derived from a <meta>. """ if resources is None: resources = tw2.core.core.request_local().get('resources', None) if resources: encoding = encoding or find_charset(html) or 'utf-8' html = MultipleReplacer.__call__( self, html, resources, encoding ) tw2.core.core.request_local().pop('resources', None) return html # Bind __call__ directly so docstring is included in docs inject_resources = _ResourceInjector().__call__ _charset_re = re.compile( r"charset\s*=\s*(?P<charset>[\w-]+)([^\>])*", re.I | re.M) def find_charset(string): m = _charset_re.search(string) if m: return m.group('charset').lower()