plugin.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. """A pytest plugin which helps testing Django applications
  2. This plugin handles creating and destroying the test environment and
  3. test database and provides some useful text fixtures.
  4. """
  5. import contextlib
  6. import inspect
  7. from functools import reduce
  8. import os
  9. import sys
  10. import types
  11. import py
  12. import pytest
  13. from .django_compat import is_django_unittest # noqa
  14. from .fixtures import django_db_setup # noqa
  15. from .fixtures import django_db_use_migrations # noqa
  16. from .fixtures import django_db_keepdb # noqa
  17. from .fixtures import django_db_modify_db_settings # noqa
  18. from .fixtures import django_db_modify_db_settings_xdist_suffix # noqa
  19. from .fixtures import _live_server_helper # noqa
  20. from .fixtures import admin_client # noqa
  21. from .fixtures import admin_user # noqa
  22. from .fixtures import client # noqa
  23. from .fixtures import db # noqa
  24. from .fixtures import django_user_model # noqa
  25. from .fixtures import django_username_field # noqa
  26. from .fixtures import live_server # noqa
  27. from .fixtures import rf # noqa
  28. from .fixtures import settings # noqa
  29. from .fixtures import transactional_db # noqa
  30. from .pytest_compat import getfixturevalue
  31. from .lazy_django import (django_settings_is_configured,
  32. get_django_version, skip_if_no_django)
  33. SETTINGS_MODULE_ENV = 'DJANGO_SETTINGS_MODULE'
  34. CONFIGURATION_ENV = 'DJANGO_CONFIGURATION'
  35. INVALID_TEMPLATE_VARS_ENV = 'FAIL_INVALID_TEMPLATE_VARS'
  36. # ############### pytest hooks ################
  37. def pytest_addoption(parser):
  38. group = parser.getgroup('django')
  39. group._addoption('--reuse-db',
  40. action='store_true', dest='reuse_db', default=False,
  41. help='Re-use the testing database if it already exists, '
  42. 'and do not remove it when the test finishes.')
  43. group._addoption('--create-db',
  44. action='store_true', dest='create_db', default=False,
  45. help='Re-create the database, even if it exists. This '
  46. 'option can be used to override --reuse-db.')
  47. group._addoption('--ds',
  48. action='store', type=str, dest='ds', default=None,
  49. help='Set DJANGO_SETTINGS_MODULE.')
  50. group._addoption('--dc',
  51. action='store', type=str, dest='dc', default=None,
  52. help='Set DJANGO_CONFIGURATION.')
  53. group._addoption('--nomigrations', '--no-migrations',
  54. action='store_true', dest='nomigrations', default=False,
  55. help='Disable Django 1.7+ migrations on test setup')
  56. group._addoption('--migrations',
  57. action='store_false', dest='nomigrations', default=False,
  58. help='Enable Django 1.7+ migrations on test setup')
  59. parser.addini(CONFIGURATION_ENV,
  60. 'django-configurations class to use by pytest-django.')
  61. group._addoption('--liveserver', default=None,
  62. help='Address and port for the live_server fixture.')
  63. parser.addini(SETTINGS_MODULE_ENV,
  64. 'Django settings module to use by pytest-django.')
  65. parser.addini('django_find_project',
  66. 'Automatically find and add a Django project to the '
  67. 'Python path.',
  68. type='bool', default=True)
  69. group._addoption('--fail-on-template-vars',
  70. action='store_true', dest='itv', default=False,
  71. help='Fail for invalid variables in templates.')
  72. parser.addini(INVALID_TEMPLATE_VARS_ENV,
  73. 'Fail for invalid variables in templates.',
  74. type='bool', default=False)
  75. def _exists(path, ignore=EnvironmentError):
  76. try:
  77. return path.check()
  78. except ignore:
  79. return False
  80. PROJECT_FOUND = ('pytest-django found a Django project in %s '
  81. '(it contains manage.py) and added it to the Python path.\n'
  82. 'If this is wrong, add "django_find_project = false" to '
  83. 'pytest.ini and explicitly manage your Python path.')
  84. PROJECT_NOT_FOUND = ('pytest-django could not find a Django project '
  85. '(no manage.py file could be found). You must '
  86. 'explicitly add your Django project to the Python path '
  87. 'to have it picked up.')
  88. PROJECT_SCAN_DISABLED = ('pytest-django did not search for Django '
  89. 'projects since it is disabled in the configuration '
  90. '("django_find_project = false")')
  91. @contextlib.contextmanager
  92. def _handle_import_error(extra_message):
  93. try:
  94. yield
  95. except ImportError as e:
  96. django_msg = (e.args[0] + '\n\n') if e.args else ''
  97. msg = django_msg + extra_message
  98. raise ImportError(msg)
  99. def _add_django_project_to_path(args):
  100. args = [x for x in args if not str(x).startswith("-")]
  101. if not args:
  102. args = [py.path.local()]
  103. for arg in args:
  104. arg = py.path.local(arg)
  105. for base in arg.parts(reverse=True):
  106. manage_py_try = base.join('manage.py')
  107. if _exists(manage_py_try):
  108. sys.path.insert(0, str(base))
  109. return PROJECT_FOUND % base
  110. return PROJECT_NOT_FOUND
  111. def _setup_django():
  112. if 'django' not in sys.modules:
  113. return
  114. import django.conf
  115. # Avoid force-loading Django when settings are not properly configured.
  116. if not django.conf.settings.configured:
  117. return
  118. django.setup()
  119. _blocking_manager.block()
  120. def _get_boolean_value(x, name, default=None):
  121. if x is None:
  122. return default
  123. if x in (True, False):
  124. return x
  125. possible_values = {'true': True,
  126. 'false': False,
  127. '1': True,
  128. '0': False}
  129. try:
  130. return possible_values[x.lower()]
  131. except KeyError:
  132. raise ValueError('{} is not a valid value for {}. '
  133. 'It must be one of {}.'
  134. % (x, name, ', '.join(possible_values.keys())))
  135. def pytest_load_initial_conftests(early_config, parser, args):
  136. # Register the marks
  137. early_config.addinivalue_line(
  138. 'markers',
  139. 'django_db(transaction=False): Mark the test as using '
  140. 'the django test database. The *transaction* argument marks will '
  141. "allow you to use real transactions in the test like Django's "
  142. 'TransactionTestCase.')
  143. early_config.addinivalue_line(
  144. 'markers',
  145. 'urls(modstr): Use a different URLconf for this test, similar to '
  146. 'the `urls` attribute of Django `TestCase` objects. *modstr* is '
  147. 'a string specifying the module of a URL config, e.g. '
  148. '"my_app.test_urls".')
  149. options = parser.parse_known_args(args)
  150. if options.version or options.help:
  151. return
  152. django_find_project = _get_boolean_value(
  153. early_config.getini('django_find_project'), 'django_find_project')
  154. if django_find_project:
  155. _django_project_scan_outcome = _add_django_project_to_path(args)
  156. else:
  157. _django_project_scan_outcome = PROJECT_SCAN_DISABLED
  158. if (options.itv or
  159. _get_boolean_value(os.environ.get(INVALID_TEMPLATE_VARS_ENV),
  160. INVALID_TEMPLATE_VARS_ENV) or
  161. early_config.getini(INVALID_TEMPLATE_VARS_ENV)):
  162. os.environ[INVALID_TEMPLATE_VARS_ENV] = 'true'
  163. # Configure DJANGO_SETTINGS_MODULE
  164. if options.ds:
  165. ds_source = 'command line option'
  166. ds = options.ds
  167. elif SETTINGS_MODULE_ENV in os.environ:
  168. ds = os.environ[SETTINGS_MODULE_ENV]
  169. ds_source = 'environment variable'
  170. elif early_config.getini(SETTINGS_MODULE_ENV):
  171. ds = early_config.getini(SETTINGS_MODULE_ENV)
  172. ds_source = 'ini file'
  173. else:
  174. ds = None
  175. ds_source = None
  176. if ds:
  177. early_config._dsm_report_header = 'Django settings: %s (from %s)' % (
  178. ds, ds_source)
  179. else:
  180. early_config._dsm_report_header = None
  181. # Configure DJANGO_CONFIGURATION
  182. dc = (options.dc or
  183. os.environ.get(CONFIGURATION_ENV) or
  184. early_config.getini(CONFIGURATION_ENV))
  185. if ds:
  186. os.environ[SETTINGS_MODULE_ENV] = ds
  187. if dc:
  188. os.environ[CONFIGURATION_ENV] = dc
  189. # Install the django-configurations importer
  190. import configurations.importer
  191. configurations.importer.install()
  192. # Forcefully load Django settings, throws ImportError or
  193. # ImproperlyConfigured if settings cannot be loaded.
  194. from django.conf import settings as dj_settings
  195. with _handle_import_error(_django_project_scan_outcome):
  196. dj_settings.DATABASES
  197. _setup_django()
  198. def pytest_report_header(config):
  199. if config._dsm_report_header:
  200. return [config._dsm_report_header]
  201. @pytest.mark.trylast
  202. def pytest_configure():
  203. # Allow Django settings to be configured in a user pytest_configure call,
  204. # but make sure we call django.setup()
  205. _setup_django()
  206. def _method_is_defined_at_leaf(cls, method_name):
  207. super_method = None
  208. for base_cls in cls.__bases__:
  209. if hasattr(base_cls, method_name):
  210. super_method = getattr(base_cls, method_name)
  211. assert super_method is not None, (
  212. '%s could not be found in base class' % method_name)
  213. return getattr(cls, method_name).__func__ is not super_method.__func__
  214. _disabled_classmethods = {}
  215. def _disable_class_methods(cls):
  216. if cls in _disabled_classmethods:
  217. return
  218. _disabled_classmethods[cls] = (
  219. cls.setUpClass,
  220. _method_is_defined_at_leaf(cls, 'setUpClass'),
  221. cls.tearDownClass,
  222. _method_is_defined_at_leaf(cls, 'tearDownClass'),
  223. )
  224. cls.setUpClass = types.MethodType(lambda cls: None, cls)
  225. cls.tearDownClass = types.MethodType(lambda cls: None, cls)
  226. def _restore_class_methods(cls):
  227. (setUpClass,
  228. restore_setUpClass,
  229. tearDownClass,
  230. restore_tearDownClass) = _disabled_classmethods.pop(cls)
  231. try:
  232. del cls.setUpClass
  233. except AttributeError:
  234. raise
  235. try:
  236. del cls.tearDownClass
  237. except AttributeError:
  238. pass
  239. if restore_setUpClass:
  240. cls.setUpClass = setUpClass
  241. if restore_tearDownClass:
  242. cls.tearDownClass = tearDownClass
  243. def pytest_runtest_setup(item):
  244. if django_settings_is_configured() and is_django_unittest(item):
  245. cls = item.cls
  246. _disable_class_methods(cls)
  247. @pytest.fixture(autouse=True, scope='session')
  248. def django_test_environment(request):
  249. """
  250. Ensure that Django is loaded and has its testing environment setup.
  251. XXX It is a little dodgy that this is an autouse fixture. Perhaps
  252. an email fixture should be requested in order to be able to
  253. use the Django email machinery just like you need to request a
  254. db fixture for access to the Django database, etc. But
  255. without duplicating a lot more of Django's test support code
  256. we need to follow this model.
  257. """
  258. if django_settings_is_configured():
  259. _setup_django()
  260. from django.conf import settings as dj_settings
  261. from django.test.utils import (setup_test_environment,
  262. teardown_test_environment)
  263. dj_settings.DEBUG = False
  264. setup_test_environment()
  265. request.addfinalizer(teardown_test_environment)
  266. @pytest.fixture(scope='session')
  267. def django_db_blocker():
  268. """Wrapper around Django's database access.
  269. This object can be used to re-enable database access. This fixture is used
  270. internally in pytest-django to build the other fixtures and can be used for
  271. special database handling.
  272. The object is a context manager and provides the methods
  273. .unblock()/.block() and .restore() to temporarily enable database access.
  274. This is an advanced feature that is meant to be used to implement database
  275. fixtures.
  276. """
  277. if not django_settings_is_configured():
  278. return None
  279. return _blocking_manager
  280. @pytest.fixture(autouse=True)
  281. def _django_db_marker(request):
  282. """Implement the django_db marker, internal to pytest-django.
  283. This will dynamically request the ``db`` or ``transactional_db``
  284. fixtures as required by the django_db marker.
  285. """
  286. marker = request.keywords.get('django_db', None)
  287. if marker:
  288. validate_django_db(marker)
  289. if marker.transaction:
  290. getfixturevalue(request, 'transactional_db')
  291. else:
  292. getfixturevalue(request, 'db')
  293. @pytest.fixture(autouse=True, scope='class')
  294. def _django_setup_unittest(request, django_db_blocker):
  295. """Setup a django unittest, internal to pytest-django."""
  296. if django_settings_is_configured() and is_django_unittest(request):
  297. getfixturevalue(request, 'django_test_environment')
  298. getfixturevalue(request, 'django_db_setup')
  299. django_db_blocker.unblock()
  300. cls = request.node.cls
  301. # implement missing (as of 1.10) debug() method for django's TestCase
  302. # see pytest-dev/pytest-django#406
  303. def _cleaning_debug(self):
  304. testMethod = getattr(self, self._testMethodName)
  305. skipped = (
  306. getattr(self.__class__, "__unittest_skip__", False) or
  307. getattr(testMethod, "__unittest_skip__", False))
  308. if not skipped:
  309. self._pre_setup()
  310. super(cls, self).debug()
  311. if not skipped:
  312. self._post_teardown()
  313. cls.debug = _cleaning_debug
  314. _restore_class_methods(cls)
  315. cls.setUpClass()
  316. _disable_class_methods(cls)
  317. def teardown():
  318. _restore_class_methods(cls)
  319. cls.tearDownClass()
  320. django_db_blocker.restore()
  321. request.addfinalizer(teardown)
  322. @pytest.fixture(scope='function', autouse=True)
  323. def _dj_autoclear_mailbox():
  324. if not django_settings_is_configured():
  325. return
  326. from django.core import mail
  327. del mail.outbox[:]
  328. @pytest.fixture(scope='function')
  329. def mailoutbox(monkeypatch, _dj_autoclear_mailbox):
  330. if not django_settings_is_configured():
  331. return
  332. from django.core import mail
  333. return mail.outbox
  334. @pytest.fixture(autouse=True, scope='function')
  335. def _django_set_urlconf(request):
  336. """Apply the @pytest.mark.urls marker, internal to pytest-django."""
  337. marker = request.keywords.get('urls', None)
  338. if marker:
  339. skip_if_no_django()
  340. import django.conf
  341. from django.core.urlresolvers import clear_url_caches, set_urlconf
  342. validate_urls(marker)
  343. original_urlconf = django.conf.settings.ROOT_URLCONF
  344. django.conf.settings.ROOT_URLCONF = marker.urls
  345. clear_url_caches()
  346. set_urlconf(None)
  347. def restore():
  348. django.conf.settings.ROOT_URLCONF = original_urlconf
  349. # Copy the pattern from
  350. # https://github.com/django/django/blob/master/django/test/signals.py#L152
  351. clear_url_caches()
  352. set_urlconf(None)
  353. request.addfinalizer(restore)
  354. @pytest.fixture(autouse=True, scope='session')
  355. def _fail_for_invalid_template_variable(request):
  356. """Fixture that fails for invalid variables in templates.
  357. This fixture will fail each test that uses django template rendering
  358. should a template contain an invalid template variable.
  359. The fail message will include the name of the invalid variable and
  360. in most cases the template name.
  361. It does not raise an exception, but fails, as the stack trace doesn't
  362. offer any helpful information to debug.
  363. This behavior can be switched off using the marker:
  364. ``ignore_template_errors``
  365. """
  366. class InvalidVarException(object):
  367. """Custom handler for invalid strings in templates."""
  368. def __init__(self):
  369. self.fail = True
  370. def __contains__(self, key):
  371. """There is a test for '%s' in TEMPLATE_STRING_IF_INVALID."""
  372. return key == '%s'
  373. def _get_template(self):
  374. from django.template import Template
  375. stack = inspect.stack()
  376. # finding the ``render`` needle in the stack
  377. frame = reduce(
  378. lambda x, y: y[3] == 'render' and 'base.py' in y[1] and y or x,
  379. stack
  380. )
  381. # assert 0, stack
  382. frame = frame[0]
  383. # finding only the frame locals in all frame members
  384. f_locals = reduce(
  385. lambda x, y: y[0] == 'f_locals' and y or x,
  386. inspect.getmembers(frame)
  387. )[1]
  388. # ``django.template.base.Template``
  389. template = f_locals['self']
  390. if isinstance(template, Template):
  391. return template
  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'" % (
  397. var, template.name)
  398. else:
  399. msg = "Undefined template variable '%s'" % var
  400. if self.fail:
  401. pytest.fail(msg, pytrace=False)
  402. else:
  403. return msg
  404. if (os.environ.get(INVALID_TEMPLATE_VARS_ENV, 'false') == 'true' and
  405. django_settings_is_configured()):
  406. from django.conf import settings as dj_settings
  407. if get_django_version() >= (1, 8) and dj_settings.TEMPLATES:
  408. dj_settings.TEMPLATES[0]['OPTIONS']['string_if_invalid'] = (
  409. InvalidVarException())
  410. else:
  411. dj_settings.TEMPLATE_STRING_IF_INVALID = InvalidVarException()
  412. @pytest.fixture(autouse=True)
  413. def _template_string_if_invalid_marker(request):
  414. """Apply the @pytest.mark.ignore_template_errors marker,
  415. internal to pytest-django."""
  416. marker = request.keywords.get('ignore_template_errors', None)
  417. if os.environ.get(INVALID_TEMPLATE_VARS_ENV, 'false') == 'true':
  418. if marker and django_settings_is_configured():
  419. from django.conf import settings as dj_settings
  420. if get_django_version() >= (1, 8) and dj_settings.TEMPLATES:
  421. dj_settings.TEMPLATES[0]['OPTIONS']['string_if_invalid'].fail = False
  422. else:
  423. dj_settings.TEMPLATE_STRING_IF_INVALID.fail = False
  424. @pytest.fixture(autouse=True, scope='function')
  425. def _django_clear_site_cache():
  426. """Clears ``django.contrib.sites.models.SITE_CACHE`` to avoid
  427. unexpected behavior with cached site objects.
  428. """
  429. if django_settings_is_configured():
  430. from django.conf import settings as dj_settings
  431. if 'django.contrib.sites' in dj_settings.INSTALLED_APPS:
  432. from django.contrib.sites.models import Site
  433. Site.objects.clear_cache()
  434. # ############### Helper Functions ################
  435. class _DatabaseBlockerContextManager(object):
  436. def __init__(self, db_blocker):
  437. self._db_blocker = db_blocker
  438. def __enter__(self):
  439. pass
  440. def __exit__(self, exc_type, exc_value, traceback):
  441. self._db_blocker.restore()
  442. class _DatabaseBlocker(object):
  443. """Manager for django.db.backends.base.base.BaseDatabaseWrapper.
  444. This is the object returned by django_db_blocker.
  445. """
  446. def __init__(self):
  447. self._history = []
  448. self._real_ensure_connection = None
  449. @property
  450. def _dj_db_wrapper(self):
  451. from .compat import BaseDatabaseWrapper
  452. # The first time the _dj_db_wrapper is accessed, we will save a
  453. # reference to the real implementation.
  454. if self._real_ensure_connection is None:
  455. self._real_ensure_connection = BaseDatabaseWrapper.ensure_connection
  456. return BaseDatabaseWrapper
  457. def _save_active_wrapper(self):
  458. return self._history.append(self._dj_db_wrapper.ensure_connection)
  459. def _blocking_wrapper(*args, **kwargs):
  460. __tracebackhide__ = True
  461. __tracebackhide__ # Silence pyflakes
  462. pytest.fail('Database access not allowed, '
  463. 'use the "django_db" mark, or the '
  464. '"db" or "transactional_db" fixtures to enable it.')
  465. def unblock(self):
  466. """Enable access to the Django database."""
  467. self._save_active_wrapper()
  468. self._dj_db_wrapper.ensure_connection = self._real_ensure_connection
  469. return _DatabaseBlockerContextManager(self)
  470. def block(self):
  471. """Disable access to the Django database."""
  472. self._save_active_wrapper()
  473. self._dj_db_wrapper.ensure_connection = self._blocking_wrapper
  474. return _DatabaseBlockerContextManager(self)
  475. def restore(self):
  476. self._dj_db_wrapper.ensure_connection = self._history.pop()
  477. _blocking_manager = _DatabaseBlocker()
  478. def validate_django_db(marker):
  479. """Validate the django_db marker.
  480. It checks the signature and creates the `transaction` attribute on
  481. the marker which will have the correct value.
  482. """
  483. def apifun(transaction=False):
  484. marker.transaction = transaction
  485. apifun(*marker.args, **marker.kwargs)
  486. def validate_urls(marker):
  487. """Validate the urls marker.
  488. It checks the signature and creates the `urls` attribute on the
  489. marker which will have the correct value.
  490. """
  491. def apifun(urls):
  492. marker.urls = urls
  493. apifun(*marker.args, **marker.kwargs)