diff --git a/flask/app.py b/flask/app.py index a818ae62..fc46a4a3 100644 --- a/flask/app.py +++ b/flask/app.py @@ -14,7 +14,7 @@ from threading import Lock from datetime import timedelta, datetime from itertools import chain -from jinja2 import Environment, PackageLoader, FileSystemLoader +from jinja2 import Environment, BaseLoader, FileSystemLoader, TemplateNotFound from werkzeug import ImmutableDict, create_environ from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError, NotFound @@ -32,13 +32,38 @@ from flask.module import _ModuleSetupState _logger_lock = Lock() -def _select_autoescape(filename): - """Returns `True` if autoescaping should be active for the given - template name. +class _DispatchingJinjaLoader(BaseLoader): + """A loader that looks for templates in the application and all + the module folders. """ - if filename is None: - return False - return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) + + def __init__(self, app): + self.app = app + + def get_source(self, environment, template): + name = template + loader = None + try: + module, name = template.split('/', 1) + loader = self.app.modules[module].jinja_loader + except (ValueError, KeyError): + pass + if loader is None: + loader = self.app.jinja_loader + try: + return loader.get_source(environment, name) + except TemplateNotFound: + # re-raise the exception with the correct fileame here. + # (the one that includes the prefix) + raise TemplateNotFound(template) + + def list_templates(self): + result = self.app.jinja_loader.list_templates() + for name, module in self.app.modules.iteritems(): + if module.jinja_loader is not None: + for template in module.jinja_loader.list_templates(): + result.append('%s/%s' % (name, template)) + return result class Flask(_PackageBoundObject): @@ -176,7 +201,6 @@ class Flask(_PackageBoundObject): #: Options that are passed directly to the Jinja2 environment. jinja_options = ImmutableDict( - autoescape=_select_autoescape, extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'] ) @@ -245,6 +269,11 @@ class Flask(_PackageBoundObject): None: [_default_template_ctx_processor] } + #: all the loaded modules in a dictionary by name. + #: + #: .. versionadded:: 0.5 + self.modules = {} + #: The :class:`~werkzeug.routing.Map` for this instance. You can use #: this to change the routing converters after the class was created #: but before any routes are connected. Example:: @@ -269,8 +298,7 @@ class Flask(_PackageBoundObject): view_func=self.send_static_file) #: The Jinja2 environment. It is created from the - #: :attr:`jinja_options` and the loader that is returned - #: by the :meth:`create_jinja_loader` function. + #: :attr:`jinja_options`. self.jinja_env = self.create_jinja_environment() self.init_jinja_globals() @@ -315,16 +343,10 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.5 """ - return Environment(loader=self.create_jinja_loader(), - **self.jinja_options) - - def create_jinja_loader(self): - """Creates the Jinja loader. By default just a package loader for - the configured package is returned that looks up templates in the - `templates` folder. To add other loaders it's possible to - override this method. - """ - return FileSystemLoader(os.path.join(self.root_path, 'templates')) + options = dict(self.jinja_options) + if 'autoescape' not in options: + options['autoescape'] = self.select_jinja_autoescape + return Environment(loader=_DispatchingJinjaLoader(self), **options) def init_jinja_globals(self): """Called directly after the environment was created to inject @@ -339,6 +361,16 @@ class Flask(_PackageBoundObject): ) self.jinja_env.filters['tojson'] = _tojson_filter + def select_jinja_autoescape(self, filename): + """Returns `True` if autoescaping should be active for the given + template name. + + .. versionadded:: 0.5 + """ + if filename is None: + return False + return filename.endswith(('.html', '.htm', '.xml', '.xhtml')) + def update_template_context(self, context): """Update the template context with some commonly used variables. This injects request, session and g into the template context. diff --git a/flask/helpers.py b/flask/helpers.py index 518d953f..d0b3ac23 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -27,7 +27,9 @@ except ImportError: except ImportError: json_available = False -from werkzeug import Headers, wrap_file, is_resource_modified +from werkzeug import Headers, wrap_file, is_resource_modified, cached_property + +from jinja2 import FileSystemLoader from flask.globals import session, _request_ctx_stack, current_app, request from flask.wrappers import Response @@ -340,6 +342,16 @@ class _PackageBoundObject(object): """ return os.path.isdir(os.path.join(self.root_path, 'static')) + @cached_property + def jinja_loader(self): + """The Jinja loader for this package bound object. + + .. versionadded:: 0.5 + """ + template_folder = os.path.join(self.root_path, 'templates') + if os.path.isdir(template_folder): + return FileSystemLoader(template_folder) + def send_static_file(self, filename): """Function used internally to send static files from the static folder to the browser. diff --git a/flask/module.py b/flask/module.py index cc904d20..ee1a342e 100644 --- a/flask/module.py +++ b/flask/module.py @@ -12,12 +12,14 @@ from flask.helpers import _PackageBoundObject -def _register_module_static(module): +def _register_module(module): """Internal helper function that returns a function for recording that registers the `send_static_file` function for the module on - the application of necessary. + the application of necessary. It also registers the module on + the application. """ - def _register_static(state): + def _register(state): + state.app.modules[module.name] = module # do not register the rule if the static folder of the # module is the same as the one from the application. if state.app.root_path == module.root_path: @@ -30,7 +32,7 @@ def _register_module_static(module): state.app.add_url_rule(path + '/', '%s.static' % module.name, view_func=module.send_static_file) - return _register_static + return _register class _ModuleSetupState(object): @@ -97,11 +99,7 @@ class Module(_PackageBoundObject): _PackageBoundObject.__init__(self, import_name) self.name = name self.url_prefix = url_prefix - self._register_events = [] - - # if there is a static folder, register it for this module - if self.has_static_folder: - self._record(_register_module_static(self)) + self._register_events = [_register_module(self)] def route(self, rule, **options): """Like :meth:`Flask.route` but for a module. The endpoint for the