Added EXPLAIN_TEMPLATE_LOADING to help people debug templates not being loaded.

This commit is contained in:
Armin Ronacher 2014-09-03 17:57:44 +02:00
parent f17ad953dd
commit bafc139810
11 changed files with 172 additions and 17 deletions

View file

@ -41,6 +41,9 @@ Version 1.0
now hardcoded but the default log handling can be disabled through the now hardcoded but the default log handling can be disabled through the
``LOGGER_HANDLER_POLICY`` configuration key. ``LOGGER_HANDLER_POLICY`` configuration key.
- Removed deprecate module functionality. - Removed deprecate module functionality.
- Added the ``EXPLAIN_TEMPLATE_LOADING`` config flag which when enabled will
instruct Flask to explain how it locates templates. This should help
users debug when the wrong templates are loaded.
Version 0.10.2 Version 0.10.2
-------------- --------------

View file

@ -185,6 +185,24 @@ want to render the template ``'admin/index.html'`` and you have provided
``templates`` as a `template_folder` you will have to create a file like ``templates`` as a `template_folder` you will have to create a file like
this: ``yourapplication/admin/templates/admin/index.html``. this: ``yourapplication/admin/templates/admin/index.html``.
To further reiterate this: if you have a blueprint named ``admin`` and you
want to render a template called ``index.html`` which is specific to this
blueprint, the best idea is to lay out your templates like this::
yourpackage/
blueprints/
admin/
templates/
admin/
index.html
__init__.py
And then when you want to render the template, use ``admin/index.html`` as
the name to look up the template by. If you encounter problems loading
the correct templates enable the ``EXPLAIN_TEMPLATE_LOADING`` config
variable which will instruct Flask to print out the steps it goes through
to locate templates on every ``render_template`` call.
Building URLs Building URLs
------------- -------------

View file

@ -188,6 +188,13 @@ The following configuration values are used internally by Flask:
be viable to disable this feature by setting be viable to disable this feature by setting
this key to ``False``. This option does not this key to ``False``. This option does not
affect debug mode. affect debug mode.
``EXPLAIN_TEMPLATE_LOADING`` If this is enabled then every attempt to
load a template will write an info
message to the logger explaining the
attempts to locate the template. This
can be useful to figure out why
templates cannot be found or wrong
templates appear to be loaded.
================================= ========================================= ================================= =========================================
.. admonition:: More on ``SERVER_NAME`` .. admonition:: More on ``SERVER_NAME``
@ -234,10 +241,8 @@ The following configuration values are used internally by Flask:
``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_PRETTYPRINT_REGULAR`` ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_PRETTYPRINT_REGULAR``
.. versionadded:: 1.0 .. versionadded:: 1.0
``SESSION_REFRESH_EACH_REQUEST`` ``SESSION_REFRESH_EACH_REQUEST``, ``TEMPLATES_AUTO_RELOAD``,
``LOGGER_HANDLER_POLICY``, ``EXPLAIN_TEMPLATE_LOADING``
.. versionadded:: 1.0
``TEMPLATES_AUTO_RELOAD``, ``LOGGER_HANDLER_POLICY``
Configuring from Files Configuring from Files
---------------------- ----------------------

View file

@ -292,6 +292,7 @@ class Flask(_PackageBoundObject):
'SEND_FILE_MAX_AGE_DEFAULT': 12 * 60 * 60, # 12 hours 'SEND_FILE_MAX_AGE_DEFAULT': 12 * 60 * 60, # 12 hours
'TRAP_BAD_REQUEST_ERRORS': False, 'TRAP_BAD_REQUEST_ERRORS': False,
'TRAP_HTTP_EXCEPTIONS': False, 'TRAP_HTTP_EXCEPTIONS': False,
'EXPLAIN_TEMPLATE_LOADING': False,
'PREFERRED_URL_SCHEME': 'http', 'PREFERRED_URL_SCHEME': 'http',
'JSON_AS_ASCII': True, 'JSON_AS_ASCII': True,
'JSON_SORT_KEYS': True, 'JSON_SORT_KEYS': True,

View file

@ -8,7 +8,10 @@
:copyright: (c) 2014 by Armin Ronacher. :copyright: (c) 2014 by Armin Ronacher.
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
from ._compat import implements_to_string from ._compat import implements_to_string, text_type
from .app import Flask
from .blueprints import Blueprint
from .globals import _request_ctx_stack
class UnexpectedUnicodeError(AssertionError, UnicodeError): class UnexpectedUnicodeError(AssertionError, UnicodeError):
@ -85,3 +88,68 @@ def attach_enctype_error_multidict(request):
newcls.__name__ = oldcls.__name__ newcls.__name__ = oldcls.__name__
newcls.__module__ = oldcls.__module__ newcls.__module__ = oldcls.__module__
request.files.__class__ = newcls request.files.__class__ = newcls
def _dump_loader_info(loader):
yield 'class: %s.%s' % (type(loader).__module__, type(loader).__name__)
for key, value in sorted(loader.__dict__.items()):
if key.startswith('_'):
continue
if isinstance(value, (tuple, list)):
if not all(isinstance(x, (str, text_type)) for x in value):
continue
yield '%s:' % key
for item in value:
yield ' - %s' % item
continue
elif not isinstance(value, (str, text_type, int, float, bool)):
continue
yield '%s: %r' % (key, value)
def explain_template_loading_attempts(app, template, attempts):
"""This should help developers understand what """
info = ['Locating template "%s":' % template]
total_found = 0
blueprint = None
reqctx = _request_ctx_stack.top
if reqctx is not None and reqctx.request.blueprint is not None:
blueprint = reqctx.request.blueprint
for idx, (loader, srcobj, triple) in enumerate(attempts):
if isinstance(srcobj, Flask):
src_info = 'application "%s"' % srcobj.import_name
elif isinstance(srcobj, Blueprint):
src_info = 'blueprint "%s" (%s)' % (srcobj.name,
srcobj.import_name)
else:
src_info = repr(srcobj)
info.append('% 5d: trying loader of %s' % (
idx + 1, src_info))
for line in _dump_loader_info(loader):
info.append(' %s' % line)
if triple is None:
detail = 'no match'
else:
detail = 'found (%r)' % (triple[1] or '<string>')
total_found += 1
info.append(' -> %s' % detail)
seems_fishy = False
if total_found == 0:
info.append('Error: the template could not be found.')
seems_fishy = True
elif total_found > 1:
info.append('Warning: multiple loaders returned a match for the template.')
seems_fishy = True
if blueprint is not None and seems_fishy:
info.append(' The template was looked up from an endpoint that '
'belongs to the blueprint "%s".' % blueprint)
info.append(' Maybe you did not place a template in the right folder?')
info.append(' See http://flask.pocoo.org/docs/blueprints/#templates')
app.logger.info('\n'.join(info))

View file

@ -71,6 +71,7 @@ def _tag(value):
try: try:
return text_type(value) return text_type(value)
except UnicodeError: except UnicodeError:
from flask.debughelpers import UnexpectedUnicodeError
raise UnexpectedUnicodeError(u'A byte string with ' raise UnexpectedUnicodeError(u'A byte string with '
u'non-ASCII data was passed to the session system ' u'non-ASCII data was passed to the session system '
u'which can only store unicode strings. Consider ' u'which can only store unicode strings. Consider '
@ -362,6 +363,3 @@ class SecureCookieSessionInterface(SessionInterface):
response.set_cookie(app.session_cookie_name, val, response.set_cookie(app.session_cookie_name, val,
expires=expires, httponly=httponly, expires=expires, httponly=httponly,
domain=domain, path=path, secure=secure) domain=domain, path=path, secure=secure)
from flask.debughelpers import UnexpectedUnicodeError

View file

@ -8,7 +8,6 @@
:copyright: (c) 2014 by Armin Ronacher. :copyright: (c) 2014 by Armin Ronacher.
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
import posixpath
from jinja2 import BaseLoader, Environment as BaseEnvironment, \ from jinja2 import BaseLoader, Environment as BaseEnvironment, \
TemplateNotFound TemplateNotFound
@ -54,23 +53,38 @@ class DispatchingJinjaLoader(BaseLoader):
self.app = app self.app = app
def get_source(self, environment, template): def get_source(self, environment, template):
for loader, local_name in self._iter_loaders(template): explain = self.app.config['EXPLAIN_TEMPLATE_LOADING']
try: attempts = []
return loader.get_source(environment, local_name) tmplrv = None
except TemplateNotFound:
pass
for srcobj, loader in self._iter_loaders(template):
try:
rv = loader.get_source(environment, template)
if tmplrv is None:
tmplrv = rv
if not explain:
break
except TemplateNotFound:
rv = None
attempts.append((loader, srcobj, rv))
if explain:
from debughelpers import explain_template_loading_attempts
explain_template_loading_attempts(self.app, template, attempts)
if tmplrv is not None:
return tmplrv
raise TemplateNotFound(template) raise TemplateNotFound(template)
def _iter_loaders(self, template): def _iter_loaders(self, template):
loader = self.app.jinja_loader loader = self.app.jinja_loader
if loader is not None: if loader is not None:
yield loader, template yield self.app, loader
for blueprint in itervalues(self.app.blueprints): for blueprint in itervalues(self.app.blueprints):
loader = blueprint.jinja_loader loader = blueprint.jinja_loader
if loader is not None: if loader is not None:
yield loader, template yield blueprint, loader
def list_templates(self): def list_templates(self):
result = set() result = set()

View file

@ -11,6 +11,9 @@
import flask import flask
import unittest import unittest
import logging
from jinja2 import TemplateNotFound
from flask.testsuite import FlaskTestCase from flask.testsuite import FlaskTestCase
@ -303,6 +306,45 @@ class TemplatingTestCase(FlaskTestCase):
app.config['TEMPLATES_AUTO_RELOAD'] = False app.config['TEMPLATES_AUTO_RELOAD'] = False
self.assert_false(app.jinja_env.auto_reload) self.assert_false(app.jinja_env.auto_reload)
def test_template_loader_debugging(self):
from blueprintapp import app
called = []
class _TestHandler(logging.Handler):
def handle(x, record):
called.append(True)
text = unicode(record.msg)
self.assert_('1: trying loader of application '
'"blueprintapp"' in text)
self.assert_('2: trying loader of blueprint "admin" '
'(blueprintapp.apps.admin)' in text)
self.assert_('trying loader of blueprint "frontend" '
'(blueprintapp.apps.frontend)' in text)
self.assert_('Error: the template could not be found' in text)
self.assert_('looked up from an endpoint that belongs to '
'the blueprint "frontend"' in text)
self.assert_(
'See http://flask.pocoo.org/docs/blueprints/#templates' in text)
with app.test_client() as c:
try:
old_load_setting = app.config['EXPLAIN_TEMPLATE_LOADING']
old_handlers = app.logger.handlers[:]
app.logger.handlers = [_TestHandler()]
app.config['EXPLAIN_TEMPLATE_LOADING'] = True
try:
c.get('/missing')
except TemplateNotFound as e:
self.assert_('missing_template.html' in str(e))
else:
self.fail('Expected template not found exception.')
finally:
app.logger.handlers[:] = old_handlers
app.config['EXPLAIN_TEMPLATE_LOADING'] = old_load_setting
self.assert_equal(len(called), 1)
def suite(): def suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()

View file

@ -1,6 +1,7 @@
from flask import Flask from flask import Flask
app = Flask(__name__) app = Flask(__name__)
app.config['DEBUG'] = True
from blueprintapp.apps.admin import admin from blueprintapp.apps.admin import admin
from blueprintapp.apps.frontend import frontend from blueprintapp.apps.frontend import frontend
app.register_blueprint(admin) app.register_blueprint(admin)

View file

@ -6,3 +6,8 @@ frontend = Blueprint('frontend', __name__, template_folder='templates')
@frontend.route('/') @frontend.route('/')
def index(): def index():
return render_template('frontend/index.html') return render_template('frontend/index.html')
@frontend.route('/missing')
def missing_template():
return render_template('missing_template.html')

View file

@ -12,7 +12,6 @@
from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase
from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequest
from .debughelpers import attach_enctype_error_multidict
from . import json from . import json
from .globals import _request_ctx_stack from .globals import _request_ctx_stack
@ -184,6 +183,7 @@ class Request(RequestBase):
ctx = _request_ctx_stack.top ctx = _request_ctx_stack.top
if ctx is not None and ctx.app.debug and \ if ctx is not None and ctx.app.debug and \
self.mimetype != 'multipart/form-data' and not self.files: self.mimetype != 'multipart/form-data' and not self.files:
from .debughelpers import attach_enctype_error_multidict
attach_enctype_error_multidict(self) attach_enctype_error_multidict(self)