|
5 | 5 | """
|
6 | 6 |
|
7 | 7 | import contextlib
|
| 8 | +import inspect |
| 9 | +from functools import reduce |
8 | 10 | import os
|
9 | 11 | import sys
|
10 | 12 | import types
|
|
27 | 29 |
|
28 | 30 | SETTINGS_MODULE_ENV = 'DJANGO_SETTINGS_MODULE'
|
29 | 31 | CONFIGURATION_ENV = 'DJANGO_CONFIGURATION'
|
| 32 | +INVALID_TEMPLATE_VARS_ENV = 'FAIL_INVALID_TEMPLATE_VARS' |
30 | 33 |
|
31 | 34 |
|
32 | 35 | # ############### pytest hooks ################
|
@@ -62,6 +65,12 @@ def pytest_addoption(parser):
|
62 | 65 | 'Automatically find and add a Django project to the '
|
63 | 66 | 'Python path.',
|
64 | 67 | default=True)
|
| 68 | + group._addoption('--fail-on-template-vars', |
| 69 | + action='store_true', dest='itv', default=False, |
| 70 | + help='Fail for invalid variables in templates.') |
| 71 | + parser.addini(INVALID_TEMPLATE_VARS_ENV, |
| 72 | + 'Fail for invalid variables in templates.', |
| 73 | + default=False) |
65 | 74 |
|
66 | 75 |
|
67 | 76 | def _exists(path, ignore=EnvironmentError):
|
@@ -170,6 +179,14 @@ def pytest_load_initial_conftests(early_config, parser, args):
|
170 | 179 | else:
|
171 | 180 | _django_project_scan_outcome = PROJECT_SCAN_DISABLED
|
172 | 181 |
|
| 182 | + # Configure FAIL_INVALID_TEMPLATE_VARS |
| 183 | + itv = (options.itv or |
| 184 | + os.environ.get(INVALID_TEMPLATE_VARS_ENV) in ['true', 'True', '1'] or |
| 185 | + early_config.getini(INVALID_TEMPLATE_VARS_ENV)) |
| 186 | + |
| 187 | + if itv: |
| 188 | + os.environ[INVALID_TEMPLATE_VARS_ENV] = 'true' |
| 189 | + |
173 | 190 | # Configure DJANGO_SETTINGS_MODULE
|
174 | 191 | ds = (options.ds or
|
175 | 192 | os.environ.get(SETTINGS_MODULE_ENV) or
|
@@ -327,6 +344,88 @@ def restore():
|
327 | 344 | request.addfinalizer(restore)
|
328 | 345 |
|
329 | 346 |
|
| 347 | +@pytest.fixture(autouse=True, scope='session') |
| 348 | +def _fail_for_invalid_template_variable(request): |
| 349 | + """Fixture that fails for invalid variables in templates. |
| 350 | +
|
| 351 | + This fixture will fail each test that uses django template rendering |
| 352 | + should a template contain an invalid template variable. |
| 353 | + The fail message will include the name of the invalid variable and |
| 354 | + in most cases the template name. |
| 355 | +
|
| 356 | + It does not raise an exception, but fails, as the stack trace doesn't |
| 357 | + offer any helpful information to debug. |
| 358 | + This behavior can be switched off using the marker: |
| 359 | + ``ignore_template_errors`` |
| 360 | + """ |
| 361 | + class InvalidVarException(object): |
| 362 | + """Custom handler for invalid strings in templates.""" |
| 363 | + |
| 364 | + def __init__(self): |
| 365 | + self.fail = True |
| 366 | + |
| 367 | + def __contains__(self, key): |
| 368 | + """There is a test for '%s' in TEMPLATE_STRING_IF_INVALID.""" |
| 369 | + return key == '%s' |
| 370 | + |
| 371 | + def _get_template(self): |
| 372 | + from django.template import Template |
| 373 | + |
| 374 | + stack = inspect.stack() |
| 375 | + # finding the ``render`` needle in the stack |
| 376 | + frame = reduce( |
| 377 | + lambda x, y: y[3] == 'render' and 'base.py' in y[1] and y or x, |
| 378 | + stack |
| 379 | + ) |
| 380 | + # assert 0, stack |
| 381 | + frame = frame[0] |
| 382 | + # finding only the frame locals in all frame members |
| 383 | + f_locals = reduce( |
| 384 | + lambda x, y: y[0] == 'f_locals' and y or x, |
| 385 | + inspect.getmembers(frame) |
| 386 | + )[1] |
| 387 | + # ``django.template.base.Template`` |
| 388 | + template = f_locals['self'] |
| 389 | + if isinstance(template, Template): |
| 390 | + return template |
| 391 | + |
| 392 | + def __mod__(self, var): |
| 393 | + """Handle TEMPLATE_STRING_IF_INVALID % var.""" |
| 394 | + template = self._get_template() |
| 395 | + if template: |
| 396 | + msg = "Undefined template variable '%s' in '%s'" % (var, template.name) |
| 397 | + else: |
| 398 | + msg = "Undefined template variable '%s'" % var |
| 399 | + if self.fail: |
| 400 | + pytest.fail(msg, pytrace=False) |
| 401 | + else: |
| 402 | + return msg |
| 403 | + if os.environ.get(INVALID_TEMPLATE_VARS_ENV, 'false') == 'true': |
| 404 | + if django_settings_is_configured(): |
| 405 | + import django |
| 406 | + from django.conf import settings |
| 407 | + |
| 408 | + if django.VERSION >= (1, 8) and settings.TEMPLATES: |
| 409 | + settings.TEMPLATES[0]['OPTIONS']['string_if_invalid'] = InvalidVarException() |
| 410 | + else: |
| 411 | + settings.TEMPLATE_STRING_IF_INVALID = InvalidVarException() |
| 412 | + |
| 413 | + |
| 414 | +@pytest.fixture(autouse=True) |
| 415 | +def _template_string_if_invalid_marker(request): |
| 416 | + """Apply the @pytest.mark.ignore_template_errors marker, |
| 417 | + internal to pytest-django.""" |
| 418 | + marker = request.keywords.get('ignore_template_errors', None) |
| 419 | + if os.environ.get(INVALID_TEMPLATE_VARS_ENV, 'false') == 'true': |
| 420 | + if marker and django_settings_is_configured(): |
| 421 | + import django |
| 422 | + from django.conf import settings |
| 423 | + |
| 424 | + if django.VERSION >= (1, 8) and settings.TEMPLATES: |
| 425 | + settings.TEMPLATES[0]['OPTIONS']['string_if_invalid'].fail = False |
| 426 | + else: |
| 427 | + settings.TEMPLATE_STRING_IF_INVALID.fail = False |
| 428 | + |
330 | 429 | # ############### Helper Functions ################
|
331 | 430 |
|
332 | 431 |
|
|
0 commit comments