From a06cd0a64418f2aafafb0a574c64c2ea5f3e9239 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 19 Mar 2011 03:28:39 +0100 Subject: [PATCH] Started work on implementing blueprint based template loading --- flask/app.py | 32 +++++++++++++++++--- flask/templating.py | 72 ++++++++++++++++++++++++++++++++++---------- flask/wrappers.py | 10 +++++- tests/flask_tests.py | 1 + 4 files changed, 93 insertions(+), 22 deletions(-) diff --git a/flask/app.py b/flask/app.py index 2bb722d4..243ae4a3 100644 --- a/flask/app.py +++ b/flask/app.py @@ -16,8 +16,6 @@ from threading import Lock from datetime import timedelta, datetime from itertools import chain -from jinja2 import Environment - from werkzeug import ImmutableDict from werkzeug.routing import Map, Rule from werkzeug.exceptions import HTTPException, InternalServerError, \ @@ -31,7 +29,7 @@ from .ctx import _RequestContext from .globals import _request_ctx_stack, request from .session import Session, _NullSession from .module import _ModuleSetupState -from .templating import _DispatchingJinjaLoader, \ +from .templating import DispatchingJinjaLoader, Environment, \ _default_template_ctx_processor from .signals import request_started, request_finished, got_request_exception @@ -280,6 +278,13 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.5 self.modules = {} + #: all the attached blueprints in a directory by name. Blueprints + #: can be attached multiple times so this dictionary does not tell + #: you how often they got attached. + #: + #: .. versionadded:: 0.7 + self.blueprints = {} + #: a place where extensions can store application specific state. For #: example this is where an extension could store database engines and #: similar things. For backwards compatibility extensions should register @@ -386,7 +391,7 @@ class Flask(_PackageBoundObject): options = dict(self.jinja_options) if 'autoescape' not in options: options['autoescape'] = self.select_jinja_autoescape - rv = Environment(loader=self.create_jinja_loader(), **options) + rv = Environment(self, **options) rv.globals.update( url_for=url_for, get_flashed_messages=get_flashed_messages @@ -400,7 +405,7 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.7 """ - return _DispatchingJinjaLoader(self) + return DispatchingJinjaLoader(self) def init_jinja_globals(self): """Deprecated. Used to initialize the Jinja2 globals. @@ -537,6 +542,10 @@ class Flask(_PackageBoundObject): of this function are the same as the ones for the constructor of the :class:`Module` class and will override the values of the module if provided. + + .. versionchanged:: 0.7 + The module system was deprecated in favor for the blueprint + system. """ if not self.enable_modules: raise RuntimeError('Module support was disabled but code ' @@ -557,6 +566,19 @@ class Flask(_PackageBoundObject): for func in module._register_events: func(state) + def register_blueprint(self, blueprint, **options): + """Registers a blueprint on the application. + + .. versionadded:: 0.7 + """ + if blueprint.name in self.blueprints: + assert self.blueprints[blueprint.name] is blueprint, \ + 'A blueprint\'s name collision ocurred between %r and ' \ + '%r.' % (blueprint, self.blueprints[blueprint.name]) + else: + self.blueprints[blueprint.name] = blueprint + blueprint.register(self, **options) + def add_url_rule(self, rule, endpoint=None, view_func=None, **options): """Connects a URL rule. Works exactly like the :meth:`route` decorator. If a view_func is provided it will be registered with the diff --git a/flask/templating.py b/flask/templating.py index 4db03b75..8e785169 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -9,7 +9,8 @@ :license: BSD, see LICENSE for more details. """ import posixpath -from jinja2 import BaseLoader, TemplateNotFound +from jinja2 import BaseLoader, Environment as BaseEnvironment, \ + TemplateNotFound from .globals import _request_ctx_stack from .signals import template_rendered @@ -28,7 +29,25 @@ def _default_template_ctx_processor(): ) -class _DispatchingJinjaLoader(BaseLoader): +class Environment(BaseEnvironment): + """Works like a regular Jinja2 environment but has some additional + knowledge of how Flask's blueprint works so that it can prepend the + name of the blueprint to referenced templates if necessary. + """ + + def __init__(self, app, **options): + if 'loader' not in options: + options['loader'] = app.create_jinja_loader() + BaseEnvironment.__init__(self, **options) + self.app = app + + def join_path(self, template, parent): + if template and template[0] == ':': + template = parent.split(':', 1)[0] + template + return template + + +class DispatchingJinjaLoader(BaseLoader): """A loader that looks for templates in the application and all the module folders. """ @@ -37,31 +56,50 @@ class _DispatchingJinjaLoader(BaseLoader): self.app = app def get_source(self, environment, template): - template = posixpath.normpath(template) - if template.startswith('../'): - raise TemplateNotFound(template) + # newstyle template support. blueprints are explicit and no further + # magic is involved. If the template cannot be loaded by the + # blueprint loader it just gives up, no further steps involved. + if ':' in template: + blueprint_name, local_template = template.split(':', 1) + local_template = posixpath.normpath(local_template) + blueprint = self.app.blueprints.get(blueprint_name) + if blueprint is None: + raise TemplateNotFound(template) + loader = blueprint.jinja_loader + if loader is not None: + return loader.get_source(environment, local_template) + + # if modules are enabled we call into the old style template lookup + # and try that before we go with the real deal. loader = None try: - module, name = template.split('/', 1) + module, name = posixpath.normpath(template).split('/', 1) loader = self.app.modules[module].jinja_loader - except (ValueError, KeyError): + except (ValueError, KeyError, TemplateNotFound): pass - # if there was a module and it has a loader, try this first - if loader is not None: - try: + try: + if loader is not None: return loader.get_source(environment, name) - except TemplateNotFound: - pass - # fall back to application loader if module failed + except TemplateNotFound: + pass + + # at the very last, load templates from the environment return self.app.jinja_loader.get_source(environment, template) def list_templates(self): - result = self.app.jinja_loader.list_templates() + result = set(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 + result.add('%s/%s' % (name, template)) + + for name, blueprint in self.app.blueprints.iteritems(): + if blueprint.jinja_loader is not None: + for template in blueprint.jinja_loader.list_templates(): + result.add('%s:%s' % (name, template)) + + return list(result) def _render(template, context, app): @@ -81,6 +119,8 @@ def render_template(template_name, **context): """ ctx = _request_ctx_stack.top ctx.app.update_template_context(context) + if template_name[:1] == ':': + template_name = ctx.request.blueprint + template_name return _render(ctx.app.jinja_env.get_template(template_name), context, ctx.app) diff --git a/flask/wrappers.py b/flask/wrappers.py index 4db1e782..422085a0 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -62,9 +62,17 @@ class Request(RequestBase): @property def module(self): """The name of the current module""" - if self.url_rule and '.' in self.url_rule.endpoint: + if self.url_rule and \ + ':' not in self.url_rule.endpoint and \ + '.' in self.url_rule.endpoint: return self.url_rule.endpoint.rsplit('.', 1)[0] + @property + def blueprint(self): + """The name of the current blueprint""" + if self.url_rule and ':' in self.url_rule.endpoint: + return self.url_rule.endpoint.split(':', 1)[0] + @cached_property def json(self): """If the mimetype is `application/json` this will contain the diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 73b7731d..f6454a88 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1111,6 +1111,7 @@ class ModuleTestCase(unittest.TestCase): def test_templates_and_static(self): app = moduleapp + app.debug = True c = app.test_client() rv = c.get('/')