From c65dc7093f31b03bca3002ee778915d6e2692bf4 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 18 Mar 2020 14:23:48 +0100 Subject: [PATCH 001/150] Remove import of pkg_resources for parsing pytest version (#826) Speeds up `pytest --version` from 249ms to 204ms. Ref: https://github.com/pytest-dev/pytest-django/pull/744#issuecomment-600581325 --- pytest_django/plugin.py | 4 ++-- tests/test_unittest.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 4deca87f3..8b6e4ce79 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -12,7 +12,6 @@ import types import pytest -from pkg_resources import parse_version from .django_compat import is_django_unittest # noqa from .fixtures import django_assert_num_queries # noqa @@ -53,7 +52,8 @@ PY2 = sys.version_info[0] == 2 # pytest 4.2 handles unittest setup/teardown itself via wrapping fixtures. -_handle_unittest_methods = parse_version(pytest.__version__) < parse_version("4.2") +_pytest_version_info = tuple(int(x) for x in pytest.__version__.split(".", 2)[:2]) +_handle_unittest_methods = _pytest_version_info < (4, 2) _report_header = [] diff --git a/tests/test_unittest.py b/tests/test_unittest.py index 985e868ed..1f637e374 100644 --- a/tests/test_unittest.py +++ b/tests/test_unittest.py @@ -1,7 +1,7 @@ import pytest from django.test import TestCase -from pkg_resources import parse_version +from pytest_django.plugin import _pytest_version_info from pytest_django_test.app.models import Item @@ -146,7 +146,7 @@ def test_pass(self): expected_lines = [ "* ERROR at setup of TestFoo.test_pass *", ] - if parse_version(pytest.__version__) < parse_version("4.2"): + if _pytest_version_info < (4, 2): expected_lines += [ "E *Failed: .setUpClass should be a classmethod", # noqa:E501 ] From 27379cbc8a3e9d12deef0ef504d4cef8aa740d25 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 19 Mar 2020 13:56:25 +0100 Subject: [PATCH 002/150] ci: tox/Travis: add pytest53 factor/job (#829) --- .travis.yml | 3 ++- tox.ini | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 777e2b7cc..20fcba839 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,8 +37,9 @@ jobs: - python: 3.6 env: TOXENV=py36-djmaster-sqlite-coverage + # Explicitly test (older) pytest 5.3. - python: 3.5 - env: TOXENV=py35-dj110-postgres-coverage + env: TOXENV=py35-dj110-postgres-pytest53-coverage services: - postgresql diff --git a/tox.ini b/tox.ini index 84bbb9859..f1ddf2beb 100644 --- a/tox.ini +++ b/tox.ini @@ -29,6 +29,7 @@ deps = pytest41: pytest>=4.1,<4.2 pytest41: attrs==17.4.0 + pytest53: pytest>=5.3,<5.4 xdist: pytest-xdist>=1.15 setenv = From 35e7697cca41b39f0a92d57bf9c2b121f9a47af4 Mon Sep 17 00:00:00 2001 From: Pierre Mourlanne Date: Fri, 20 Mar 2020 23:20:58 +0100 Subject: [PATCH 003/150] Improve test ordering with Django test classes (#830) --- pytest_django/plugin.py | 13 +++++++++++++ tests/test_db_setup.py | 22 ++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 8b6e4ce79..5204da30c 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -417,7 +417,20 @@ def pytest_runtest_setup(item): @pytest.hookimpl(tryfirst=True) def pytest_collection_modifyitems(items): + # If Django is not configured we don't need to bother + if not django_settings_is_configured(): + return + + from django.test import TestCase, TransactionTestCase + def get_order_number(test): + if hasattr(test, "cls") and test.cls: + # Beware, TestCase is a subclass of TransactionTestCase + if issubclass(test.cls, TestCase): + return 0 + if issubclass(test.cls, TransactionTestCase): + return 1 + marker_db = test.get_closest_marker('django_db') if marker_db: transaction = validate_django_db(marker_db)[0] diff --git a/tests/test_db_setup.py b/tests/test_db_setup.py index 5b8fcd38c..375cb8449 100644 --- a/tests/test_db_setup.py +++ b/tests/test_db_setup.py @@ -33,7 +33,9 @@ def test_db_order(django_testdir): """Test order in which tests are being executed.""" django_testdir.create_test_module(''' + from unittest import TestCase import pytest + from django.test import SimpleTestCase, TestCase as DjangoTestCase, TransactionTestCase from .app.models import Item @@ -50,14 +52,34 @@ def test_run_first_fixture(db): @pytest.mark.django_db def test_run_first_decorator(): pass + + class MyTestCase(TestCase): + def test_run_last_test_case(self): + pass + + class MySimpleTestCase(SimpleTestCase): + def test_run_last_simple_test_case(self): + pass + + class MyDjangoTestCase(DjangoTestCase): + def test_run_first_django_test_case(self): + pass + + class MyTransactionTestCase(TransactionTestCase): + def test_run_second_transaction_test_case(self): + pass ''') result = django_testdir.runpytest_subprocess('-v', '-s') assert result.ret == 0 result.stdout.fnmatch_lines([ "*test_run_first_fixture*", "*test_run_first_decorator*", + "*test_run_first_django_test_case*", "*test_run_second_decorator*", "*test_run_second_fixture*", + "*test_run_second_transaction_test_case*", + "*test_run_last_test_case*", + "*test_run_last_simple_test_case*", ]) From 203a7d64c9cc74e5bdddd98cf0aa456303b24c0a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 22 Mar 2020 18:02:37 +0100 Subject: [PATCH 004/150] tox: unset extras with non-default factors (#831) "extras" gets inherited from "testenv". --- tox.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tox.ini b/tox.ini index f1ddf2beb..2a75506b4 100644 --- a/tox.ini +++ b/tox.ini @@ -56,6 +56,7 @@ commands = coverage: coverage xml [testenv:checkqa] +extras = deps = flake8 commands = @@ -63,6 +64,7 @@ commands = flake8 --statistics {posargs:pytest_django pytest_django_test tests} [testenv:doc8] +extras = basepython = python3.6 skip_install = true deps = @@ -77,6 +79,7 @@ extras = docs commands = sphinx-build -n -W -b html -d docs/_build/doctrees docs docs/_build/html [testenv:readme] +extras = basepython = python3.5 deps = readme_renderer From b3a8a9e6fb4f0390950d1e4adc4f960d096bd673 Mon Sep 17 00:00:00 2001 From: Christian Long Date: Tue, 24 Mar 2020 12:29:09 -0500 Subject: [PATCH 005/150] Explain that the `db` fixture returns None I was thinking that I could use the `db` fixture to access a database connection object within a test. However, the `db` fixture returns None. This documentation update makes it clear that the `db` fixture does not return an object, and explains that users should use from `django.db import connection` instead. Reference: See [this comment](https://github.com/pytest-dev/pytest-django/issues/338#issuecomment-227402089) on issue #338. --- docs/helpers.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/helpers.rst b/docs/helpers.rst index 76b32492a..03434faf7 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -231,7 +231,10 @@ is configured to be in the containing Django project. This fixture will ensure the Django database is set up. Only required for fixtures that want to use the database themselves. A test function should normally use the ``pytest.mark.django_db`` -mark to signal it needs the database. +mark to signal it needs the database. This fixture does +not return a database connection object. When you need a Django +database connection or cursor, import it from Django using +``from django.db import connection``. ``transactional_db`` ~~~~~~~~~~~~~~~~~~~~ From de97bca77864fc85051adf6b8d3a08e7b04d549e Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 30 Mar 2020 23:46:48 +0200 Subject: [PATCH 006/150] pytest_addoption: use `group.addoption` (#833) Using `group._addoption` bypasses the check for existing options. --- pytest_django/plugin.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 5204da30c..b5d34ba14 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -63,7 +63,7 @@ def pytest_addoption(parser): group = parser.getgroup("django") - group._addoption( + group.addoption( "--reuse-db", action="store_true", dest="reuse_db", @@ -71,7 +71,7 @@ def pytest_addoption(parser): help="Re-use the testing database if it already exists, " "and do not remove it when the test finishes.", ) - group._addoption( + group.addoption( "--create-db", action="store_true", dest="create_db", @@ -79,7 +79,7 @@ def pytest_addoption(parser): help="Re-create the database, even if it exists. This " "option can be used to override --reuse-db.", ) - group._addoption( + group.addoption( "--ds", action="store", type=str, @@ -87,7 +87,7 @@ def pytest_addoption(parser): default=None, help="Set DJANGO_SETTINGS_MODULE.", ) - group._addoption( + group.addoption( "--dc", action="store", type=str, @@ -95,7 +95,7 @@ def pytest_addoption(parser): default=None, help="Set DJANGO_CONFIGURATION.", ) - group._addoption( + group.addoption( "--nomigrations", "--no-migrations", action="store_true", @@ -103,7 +103,7 @@ def pytest_addoption(parser): default=False, help="Disable Django migrations on test setup", ) - group._addoption( + group.addoption( "--migrations", action="store_false", dest="nomigrations", @@ -113,7 +113,7 @@ def pytest_addoption(parser): parser.addini( CONFIGURATION_ENV, "django-configurations class to use by pytest-django." ) - group._addoption( + group.addoption( "--liveserver", default=None, help="Address and port for the live_server fixture.", @@ -128,7 +128,7 @@ def pytest_addoption(parser): type="bool", default=True, ) - group._addoption( + group.addoption( "--fail-on-template-vars", action="store_true", dest="itv", From 001dc964574ccc62bf79a3687027b5fe7d0ed0d8 Mon Sep 17 00:00:00 2001 From: Adam Parkin Date: Mon, 30 Mar 2020 15:04:12 -0700 Subject: [PATCH 007/150] docs: fix typo/grammar in database docs (#834) Co-authored-by: Daniel Hahler --- docs/database.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/database.rst b/docs/database.rst index 75eb2eda6..347312d6c 100644 --- a/docs/database.rst +++ b/docs/database.rst @@ -401,7 +401,7 @@ Use the same database for all xdist processes """"""""""""""""""""""""""""""""""""""""""""" By default, each xdist process gets its own database to run tests on. This is -needed to have transactional tests that does not interfere with eachother. +needed to have transactional tests that do not interfere with each other. If you instead want your tests to use the same database, override the :fixture:`django_db_modify_db_settings` to not do anything. Put this in From 162263f338d863fddc14b0659f506d63799f78e1 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 31 Mar 2020 11:41:21 +0200 Subject: [PATCH 008/150] Work around unittest issue with pytest 5.4.{0,1} (#825) * Work around unittest issue with pytest 5.4.{0,1} Ref: https://github.com/pytest-dev/pytest-django/issues/824 * tox/travis: pytest53 --- pytest_django/plugin.py | 20 ++++++++------------ tests/test_unittest.py | 24 ++++++++++++++++++++---- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index b5d34ba14..5c59bd540 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -520,20 +520,16 @@ def _django_setup_unittest(request, django_db_blocker): yield return - from _pytest.unittest import TestCaseFunction + # Fix/patch pytest. + # Before pytest 5.4: https://github.com/pytest-dev/pytest/issues/5991 + # After pytest 5.4: https://github.com/pytest-dev/pytest-django/issues/824 + from _pytest.monkeypatch import MonkeyPatch - if "debug" in TestCaseFunction.runtest.__code__.co_names: - # Fix pytest (https://github.com/pytest-dev/pytest/issues/5991), only - # if "self._testcase.debug()" is being used (forward compatible). - from _pytest.monkeypatch import MonkeyPatch + def non_debugging_runtest(self): + self._testcase(result=self) - def non_debugging_runtest(self): - self._testcase(result=self) - - mp_debug = MonkeyPatch() - mp_debug.setattr("_pytest.unittest.TestCaseFunction.runtest", non_debugging_runtest) - else: - mp_debug = None + mp_debug = MonkeyPatch() + mp_debug.setattr("_pytest.unittest.TestCaseFunction.runtest", non_debugging_runtest) request.getfixturevalue("django_db_setup") diff --git a/tests/test_unittest.py b/tests/test_unittest.py index 1f637e374..1f6dcd55c 100644 --- a/tests/test_unittest.py +++ b/tests/test_unittest.py @@ -58,10 +58,11 @@ def tearDown(self): def test_sole_test(django_testdir): """ - Make sure the database are configured when only Django TestCase classes + Make sure the database is configured when only Django TestCase classes are collected, without the django_db marker. - """ + Also ensures that the DB is available after a failure (#824). + """ django_testdir.create_test_module( """ import os @@ -80,12 +81,27 @@ def test_foo(self): # Make sure it is usable assert Item.objects.count() == 0 + + assert 0, "trigger_error" + + class TestBar(TestCase): + def test_bar(self): + assert Item.objects.count() == 0 """ ) result = django_testdir.runpytest_subprocess("-v") - result.stdout.fnmatch_lines(["*TestFoo*test_foo PASSED*"]) - assert result.ret == 0 + result.stdout.fnmatch_lines( + [ + "*::test_foo FAILED", + "*::test_bar PASSED", + '> assert 0, "trigger_error"', + "E AssertionError: trigger_error", + "E assert 0", + "*= 1 failed, 1 passed in *", + ] + ) + assert result.ret == 1 class TestUnittestMethods: From ee7858af1a80e0091af4d260c0a70e5766c1196a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 31 Mar 2020 11:47:20 +0200 Subject: [PATCH 009/150] Release 3.9.0 --- docs/changelog.rst | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 645aee784..f76e58226 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,37 @@ Changelog ========= +v3.9.0 (2020-03-31) +------------------- + +Improvements +^^^^^^^^^^^^ + +* Improve test ordering with Django test classes (#830) + +* Remove import of pkg_resources for parsing pytest version (performance) (#826) + +Bugfixes +^^^^^^^^ + +* Work around unittest issue with pytest 5.4.{0,1} (#825) + +* Don't break --failed-first when re-ordering tests (#819, #820) + +* pytest_addoption: use `group.addoption` (#833) + +Misc +^^^^ + +* Remove Django version from --nomigrations heading (#822) + +* docs: changelog: prefix headers with v for permalink anchors + +* changelog: add custom/fixed anchor for last version + +* setup.py: add Changelog to project_urls + + v3.8.0 (2020-01-14) -------------------- From b8297549d2488c014279ccb76f482e99afacb7cf Mon Sep 17 00:00:00 2001 From: Tony De Leon Date: Mon, 25 May 2020 13:02:43 -0400 Subject: [PATCH 010/150] Fix incorrect sentence syntax. In this sentence: "You need to tell pytest which Django settings that should be used for test runs." The word "that" should not be used. The correct wording should be: "You need to tell pytest which Django settings should be used for test runs." --- docs/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 354af702d..ee4ec00c2 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -42,7 +42,7 @@ after installation, there is nothing more to configure. Step 2: Point pytest to your Django settings -------------------------------------------- -You need to tell pytest which Django settings that should be used for test +You need to tell pytest which Django settings should be used for test runs. The easiest way to achieve this is to create a pytest configuration file with this information. From 400143b55f503ca7cb23d5bd63844fd7be4fff0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=A4ufl?= Date: Sun, 2 Aug 2020 10:41:15 +0200 Subject: [PATCH 011/150] Fix invalid format string --- pytest_django/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 5c59bd540..4d692de27 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -240,7 +240,7 @@ def _get_boolean_value(x, name, default=None): except KeyError: raise ValueError( "{} is not a valid value for {}. " - "It must be one of {}." % (x, name, ", ".join(possible_values.keys())) + "It must be one of {}.".format(x, name, ", ".join(possible_values.keys())) ) From 3f03d0a7890e987086042b42db346e47398ffed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=A4ufl?= Date: Sun, 2 Aug 2020 10:41:31 +0200 Subject: [PATCH 012/150] Fix compat with pytest 6 --- tests/test_manage_py_scan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_manage_py_scan.py b/tests/test_manage_py_scan.py index 8a0f9aad3..a11f87c24 100644 --- a/tests/test_manage_py_scan.py +++ b/tests/test_manage_py_scan.py @@ -116,7 +116,7 @@ def test_django_project_found_invalid_settings_version(django_testdir, monkeypat """Invalid DSM should not cause an error with --help or --version.""" monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "DOES_NOT_EXIST") - result = django_testdir.runpytest_subprocess("django_project_root", "--version") + result = django_testdir.runpytest_subprocess("django_project_root", "--version", "--version") assert result.ret == 0 result.stderr.fnmatch_lines(["*This is pytest version*"]) From 5ed3ea203eccd691c405f58a5874d531b4df9959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=A4ufl?= Date: Sun, 2 Aug 2020 10:26:07 +0200 Subject: [PATCH 013/150] Run tests against Django 3.1 --- .travis.yml | 2 ++ setup.py | 1 + tests/test_fixtures.py | 10 ++++++++-- tox.ini | 9 +++++---- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 20fcba839..d4f70d0eb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,6 +29,8 @@ jobs: env: TOXENV=py37-dj22-sqlite-xdist-coverage - python: 3.8 env: TOXENV=py38-dj30-sqlite-xdist-coverage + - python: 3.8 + env: TOXENV=py38-dj31-sqlite-xdist-coverage # Explicitly test (older) pytest 4.1. - python: 3.7 diff --git a/setup.py b/setup.py index 217593232..8513c299a 100755 --- a/setup.py +++ b/setup.py @@ -55,6 +55,7 @@ def read(fname): 'Framework :: Django :: 2.1', 'Framework :: Django :: 2.2', 'Framework :: Django :: 3.0', + 'Framework :: Django :: 3.1', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 2c718c54f..fae054350 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -516,10 +516,16 @@ class MyCustomUser(AbstractUser): ) django_testdir.create_app_file( """ - from django.conf.urls import url from tpkg.app import views - urlpatterns = [url(r'admin-required/', views.admin_required_view)] + try: + from django.urls import path + except ImportError: + from django.conf.urls import url + + urlpatterns = [url(r'admin-required/', views.admin_required_view)] + else: + urlpatterns = [path('admin-required/', views.admin_required_view)] """, "urls.py", ) diff --git a/tox.ini b/tox.ini index 2a75506b4..d6fb991e4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = - py37-dj{30,22,21,20,111}-postgres - py36-dj{30,22,21,20,111,110,19,18}-postgres + py37-dj{31,30,22,21,20,111}-postgres + py36-dj{31,30,22,21,20,111,110,19,18}-postgres py35-dj{22,21,20,111,110,19,18}-postgres py34-dj{20,111,110}-postgres py27-dj{111,110}-{mysql_innodb,mysql_myisam,postgres} @@ -12,8 +12,9 @@ envlist = extras = testing deps = djmaster: https://github.com/django/django/archive/master.tar.gz - dj30: Django>=3.0a1,<3.1 - dj22: Django>=2.2a1,<2.3 + dj31: Django>=3.1rc1,<3.2 + dj30: Django>=3.0,<3.1 + dj22: Django>=2.2,<2.3 dj21: Django>=2.1,<2.2 dj20: Django>=2.0,<2.1 dj111: Django>=1.11,<1.12 From ed04bb0613fb14885efe939d38a7d6ae23a12435 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Sat, 15 Aug 2020 00:01:22 +0200 Subject: [PATCH 014/150] Fix support for Django 3.2 --- pytest_django/compat.py | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/pytest_django/compat.py b/pytest_django/compat.py index 9203a50e1..fad32d63d 100644 --- a/pytest_django/compat.py +++ b/pytest_django/compat.py @@ -1,8 +1,29 @@ # This file cannot be imported from until Django sets up -try: - # Django 1.11 - from django.test.utils import setup_databases, teardown_databases # noqa: F401 -except ImportError: + + +def _get_setup_and_teardown_databases(): + try: + # Django 3.2+ has added timing capabilities that we don't really support + # right now. Unfortunately that new time_keeper is required. + from django.test.utils import NullTimeKeeper + except ImportError: + pass + else: + from django.test.utils import setup_databases, teardown_databases + + def wrapped_setup_databases(*args, **kwargs): + return setup_databases(*args, time_keeper=NullTimeKeeper(), **kwargs) + + return wrapped_setup_databases, teardown_databases + + try: + # Django 1.11+ + from django.test.utils import setup_databases, teardown_databases # noqa: F401 + except ImportError: + pass + else: + return setup_databases, teardown_databases + # In Django prior to 1.11, teardown_databases is only available as a method on DiscoverRunner from django.test.runner import ( # noqa: F401 setup_databases, @@ -13,3 +34,9 @@ def teardown_databases(db_cfg, verbosity): _DiscoverRunner(verbosity=verbosity, interactive=False).teardown_databases( db_cfg ) + + return setup_databases, teardown_databases + + +setup_databases, teardown_databases = _get_setup_and_teardown_databases() +del _get_setup_and_teardown_databases From 85ee34542c59625c564a8c7fab9ba91606f522d4 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Sat, 15 Aug 2020 00:21:11 +0200 Subject: [PATCH 015/150] fix lints --- pytest_django/compat.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pytest_django/compat.py b/pytest_django/compat.py index fad32d63d..edb73d6dd 100644 --- a/pytest_django/compat.py +++ b/pytest_django/compat.py @@ -18,20 +18,17 @@ def wrapped_setup_databases(*args, **kwargs): try: # Django 1.11+ - from django.test.utils import setup_databases, teardown_databases # noqa: F401 + from django.test.utils import setup_databases, teardown_databases # noqa: F401, F811 except ImportError: pass else: return setup_databases, teardown_databases # In Django prior to 1.11, teardown_databases is only available as a method on DiscoverRunner - from django.test.runner import ( # noqa: F401 - setup_databases, - DiscoverRunner as _DiscoverRunner, - ) + from django.test.runner import setup_databases, DiscoverRunner # noqa: F401, F811 def teardown_databases(db_cfg, verbosity): - _DiscoverRunner(verbosity=verbosity, interactive=False).teardown_databases( + DiscoverRunner(verbosity=verbosity, interactive=False).teardown_databases( db_cfg ) From 107155e09b509c73d597b57a66d4865b4e44407d Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sun, 16 Aug 2020 20:22:15 +0100 Subject: [PATCH 016/150] support pytest-xdist 2 --- pytest_django/fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index b2cc82580..ce8111f1e 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -46,7 +46,7 @@ def django_db_modify_db_settings_tox_suffix(): def django_db_modify_db_settings_xdist_suffix(request): skip_if_no_django() - xdist_suffix = getattr(request.config, "slaveinput", {}).get("slaveid") + xdist_suffix = getattr(request.config, "workerinput", {}).get("workerid") if xdist_suffix: # Put a suffix like _gw0, _gw1 etc on xdist processes _set_suffix_to_test_databases(suffix=xdist_suffix) From 7474f9bee92979f5a53d92544e8f5a7cf1de67b7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 25 Aug 2020 18:03:39 +0300 Subject: [PATCH 017/150] Release 3.10.0 --- docs/changelog.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index f76e58226..18fb35be0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,25 @@ Changelog ========= +v3.10.0 (2020-08-25) +-------------------- + +Improvements +^^^^^^^^^^^^ + +* Officialy support Django 3.1 + +* Preliminary supoprt for upcoming Django 3.2 + +* Support for pytest-xdist 2.0 + + +Misc +^^^^ + +* Fix running pytest-django's own tests against pytest 6.0 (#855) + + v3.9.0 (2020-03-31) ------------------- From 588a510c81d937d5f27a0000a001edb077e674a9 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Thu, 3 Sep 2020 09:55:20 +0200 Subject: [PATCH 018/150] Revert workaround for not optional django.test.utils.setup_databases()'s time_keeper. time_keeper is optional since https://github.com/django/django/commit/0b8871ab6744285943784795ede053839ee009ef This reverts ed04bb0613fb14885efe939d38a7d6ae23a12435. --- pytest_django/compat.py | 35 ++++------------------------------- 1 file changed, 4 insertions(+), 31 deletions(-) diff --git a/pytest_django/compat.py b/pytest_django/compat.py index edb73d6dd..97a847c24 100644 --- a/pytest_django/compat.py +++ b/pytest_django/compat.py @@ -1,29 +1,8 @@ # This file cannot be imported from until Django sets up - - -def _get_setup_and_teardown_databases(): - try: - # Django 3.2+ has added timing capabilities that we don't really support - # right now. Unfortunately that new time_keeper is required. - from django.test.utils import NullTimeKeeper - except ImportError: - pass - else: - from django.test.utils import setup_databases, teardown_databases - - def wrapped_setup_databases(*args, **kwargs): - return setup_databases(*args, time_keeper=NullTimeKeeper(), **kwargs) - - return wrapped_setup_databases, teardown_databases - - try: - # Django 1.11+ - from django.test.utils import setup_databases, teardown_databases # noqa: F401, F811 - except ImportError: - pass - else: - return setup_databases, teardown_databases - +try: + # Django 1.11+ + from django.test.utils import setup_databases, teardown_databases # noqa: F401, F811 +except ImportError: # In Django prior to 1.11, teardown_databases is only available as a method on DiscoverRunner from django.test.runner import setup_databases, DiscoverRunner # noqa: F401, F811 @@ -31,9 +10,3 @@ def teardown_databases(db_cfg, verbosity): DiscoverRunner(verbosity=verbosity, interactive=False).teardown_databases( db_cfg ) - - return setup_databases, teardown_databases - - -setup_databases, teardown_databases = _get_setup_and_teardown_databases() -del _get_setup_and_teardown_databases From ce61e0446f11b4a37077800a4cbb51153c9be2f4 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 18 Sep 2020 10:10:48 +0300 Subject: [PATCH 019/150] Drop support for Python<3.5, Django<2.2, pytest<5.4 --- .travis.yml | 56 ++++++-------- README.rst | 10 ++- docs/conf.py | 2 - docs/faq.rst | 2 +- pytest_django/asserts.py | 8 +- pytest_django/compat.py | 12 --- pytest_django/fixtures.py | 36 ++------- pytest_django/live_server_helper.py | 55 +++----------- pytest_django/migrations.py | 7 +- pytest_django/plugin.py | 75 +++++-------------- .../app/migrations/0001_initial.py | 2 - pytest_django_test/compat.py | 4 - pytest_django_test/db_helpers.py | 12 +-- pytest_django_test/settings_base.py | 5 -- pytest_django_test/settings_postgres.py | 6 +- pytest_django_test/urls.py | 6 +- pytest_django_test/urls_overridden.py | 4 +- setup.py | 15 +--- tests/conftest.py | 8 +- tests/test_database.py | 2 - tests/test_db_setup.py | 29 +------ tests/test_django_settings_module.py | 25 +------ tests/test_environment.py | 64 +++++----------- tests/test_fixtures.py | 57 +++----------- tests/test_unittest.py | 15 +--- tests/test_urls.py | 27 +++---- tox.ini | 27 +++---- 27 files changed, 142 insertions(+), 429 deletions(-) delete mode 100644 pytest_django/compat.py delete mode 100644 pytest_django_test/compat.py diff --git a/.travis.yml b/.travis.yml index d4f70d0eb..60ecef3d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,71 +1,61 @@ language: python -dist: xenial +dist: focal cache: false jobs: fast_finish: true include: - stage: baseline - python: 3.6 + python: 3.8 env: - - TOXENV=py36-dj20-postgres-xdist-coverage + - TOXENV=py38-dj31-postgres-xdist-coverage # Test in verbose mode. - PYTEST_ADDOPTS=-vv services: - postgresql - - python: 3.6 - env: TOXENV=py36-dj111-mysql_innodb-coverage + - python: 3.7 + env: TOXENV=py37-dj30-mysql_innodb-coverage services: - mysql - - python: 2.7 - env: TOXENV=py27-dj111-sqlite-xdist-coverage - python: 3.6 + env: TOXENV=py36-dj22-sqlite-xdist-coverage + - python: 3.8 env: TOXENV=checkqa,docs - stage: test python: 3.7 - env: TOXENV=py37-dj21-sqlite-coverage - - python: 3.7 env: TOXENV=py37-dj22-sqlite-xdist-coverage - python: 3.8 env: TOXENV=py38-dj30-sqlite-xdist-coverage - python: 3.8 env: TOXENV=py38-dj31-sqlite-xdist-coverage - # Explicitly test (older) pytest 4.1. - - python: 3.7 - env: TOXENV=py37-dj21-sqlite-pytest41-coverage - - - python: 3.6 - env: TOXENV=py36-djmaster-sqlite-coverage + - python: 3.8 + env: TOXENV=py38-djmaster-sqlite-coverage - # Explicitly test (older) pytest 5.3. + # Explicitly test (older) pytest 5.4. - python: 3.5 - env: TOXENV=py35-dj110-postgres-pytest53-coverage + env: TOXENV=py35-dj22-postgres-pytest54-coverage services: - postgresql - - python: 3.4 - env: TOXENV=py34-dj19-sqlite_file-coverage + - python: 3.5 + env: TOXENV=py35-dj22-sqlite_file-coverage - - python: 2.7 - env: TOXENV=py27-dj111-mysql_myisam-coverage + - python: 3.6 + env: TOXENV=py36-dj31-mysql_myisam-coverage services: - mysql - - python: 2.7 - env: TOXENV=py27-dj18-postgres-coverage - services: - - postgresql - # pypy/pypy3: not included with coverage reports (much slower then). - - python: pypy - env: TOXENV=pypy-dj111-sqlite_file + # pypy3: not included with coverage reports (much slower then). - python: pypy3 - env: TOXENV=pypy3-dj110-sqlite + env: TOXENV=pypy3-dj22-postgres + services: + - postgresql - stage: test_release - python: 3.6 - env: TOXENV=py36-dj20-postgres + python: 3.8 + env: TOXENV=py38-dj31-postgres services: - postgresql @@ -85,7 +75,7 @@ jobs: # NOTE: does not show up in "allowed failures" section, but is allowed to # fail (for the "test" stage). allow_failures: - - env: TOXENV=py36-djmaster-sqlite-coverage + - env: TOXENV=py38-djmaster-sqlite-coverage stages: - name: baseline @@ -98,7 +88,7 @@ stages: if: tag IS present install: - - pip install tox==3.9.0 + - pip install tox==3.20.0 script: - tox diff --git a/README.rst b/README.rst index 47255c882..367367c63 100644 --- a/README.rst +++ b/README.rst @@ -28,10 +28,12 @@ pytest-django allows you to test your Django project/applications with the `_ * Version compatibility: - * Django: 1.8-1.11, 2.0-2.2, - and latest master branch (compatible at the time of each release) - * Python: CPython 2.7, 3.4-3.7 or PyPy 2, 3 - * pytest: >=3.6 + * Django: 2.2, 3.0, 3.1 and latest master branch (compatible at the time of + each release) + * Python: CPython>=3.5 or PyPy 3 + * pytest: >=5.4 + + For compatibility with older versions, use the pytest-django 3.*.* series. * Licence: BSD * Project maintainers: Andreas Pelme, Floris Bruynooghe and Daniel Hahler diff --git a/docs/conf.py b/docs/conf.py index c64d2d2a2..b049c68ee 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import os import sys import datetime diff --git a/docs/faq.rst b/docs/faq.rst index 1c6be1be2..432e47f48 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -76,7 +76,7 @@ test runner like this: .. code-block:: python - class PytestTestRunner(object): + class PytestTestRunner: """Runs pytest to discover and run tests.""" def __init__(self, verbosity=1, failfast=False, keepdb=False, **kwargs): diff --git a/pytest_django/asserts.py b/pytest_django/asserts.py index 35fe979ba..12a3fc565 100644 --- a/pytest_django/asserts.py +++ b/pytest_django/asserts.py @@ -23,10 +23,10 @@ def assertion_func(*args, **kwargs): __all__ = [] assertions_names = set() assertions_names.update( - set(attr for attr in vars(TestCase) if attr.startswith('assert')), - set(attr for attr in vars(SimpleTestCase) if attr.startswith('assert')), - set(attr for attr in vars(LiveServerTestCase) if attr.startswith('assert')), - set(attr for attr in vars(TransactionTestCase) if attr.startswith('assert')), + {attr for attr in vars(TestCase) if attr.startswith('assert')}, + {attr for attr in vars(SimpleTestCase) if attr.startswith('assert')}, + {attr for attr in vars(LiveServerTestCase) if attr.startswith('assert')}, + {attr for attr in vars(TransactionTestCase) if attr.startswith('assert')}, ) for assert_func in assertions_names: diff --git a/pytest_django/compat.py b/pytest_django/compat.py deleted file mode 100644 index 97a847c24..000000000 --- a/pytest_django/compat.py +++ /dev/null @@ -1,12 +0,0 @@ -# This file cannot be imported from until Django sets up -try: - # Django 1.11+ - from django.test.utils import setup_databases, teardown_databases # noqa: F401, F811 -except ImportError: - # In Django prior to 1.11, teardown_databases is only available as a method on DiscoverRunner - from django.test.runner import setup_databases, DiscoverRunner # noqa: F401, F811 - - def teardown_databases(db_cfg, verbosity): - DiscoverRunner(verbosity=verbosity, interactive=False).teardown_databases( - db_cfg - ) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index ce8111f1e..0f2dd6115 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -1,9 +1,7 @@ """All pytest-django fixtures""" -from __future__ import with_statement import os -import warnings from contextlib import contextmanager from functools import partial @@ -91,7 +89,7 @@ def django_db_setup( django_db_modify_db_settings, ): """Top level fixture to ensure test databases are available""" - from .compat import setup_databases, teardown_databases + from django.test.utils import setup_databases, teardown_databases setup_databases_args = {} @@ -164,7 +162,7 @@ def _disable_native_migrations(): class MigrateSilentCommand(migrate.Command): def handle(self, *args, **kwargs): kwargs["verbosity"] = 0 - return super(MigrateSilentCommand, self).handle(*args, **kwargs) + return super().handle(*args, **kwargs) migrate.Command = MigrateSilentCommand @@ -320,7 +318,7 @@ def rf(): return RequestFactory() -class SettingsWrapper(object): +class SettingsWrapper: _to_restore = [] def __delattr__(self, attr): @@ -370,8 +368,8 @@ def live_server(request): The address the server is started from is taken from the --liveserver command line option or if this is not provided from the DJANGO_LIVE_TEST_SERVER_ADDRESS environment variable. If - neither is provided ``localhost:8081,8100-8200`` is used. See the - Django documentation for its full syntax. + neither is provided ``localhost`` is used. See the Django + documentation for its full syntax. NOTE: If the live server needs database access to handle a request your test will have to request database access. Furthermore @@ -385,27 +383,9 @@ def live_server(request): """ skip_if_no_django() - import django - addr = request.config.getvalue("liveserver") or os.getenv( "DJANGO_LIVE_TEST_SERVER_ADDRESS" - ) - - if addr and ":" in addr: - if django.VERSION >= (1, 11): - ports = addr.split(":")[1] - if "-" in ports or "," in ports: - warnings.warn( - "Specifying multiple live server ports is not supported " - "in Django 1.11. This will be an error in a future " - "pytest-django release." - ) - - if not addr: - if django.VERSION < (1, 11): - addr = "localhost:8081,8100-8200" - else: - addr = "localhost" + ) or "localhost" server = live_server_helper.LiveServer(addr) request.addfinalizer(server.stop) @@ -458,14 +438,14 @@ def _assert_num_queries(config, num, exact=True, connection=None, info=None): num, "" if exact else "or less ", "but {} done".format( - num_performed == 1 and "1 was" or "%d were" % (num_performed,) + num_performed == 1 and "1 was" or "{} were".format(num_performed) ), ) if info: msg += "\n{}".format(info) if verbose: sqls = (q["sql"] for q in context.captured_queries) - msg += "\n\nQueries:\n========\n\n%s" % "\n\n".join(sqls) + msg += "\n\nQueries:\n========\n\n" + "\n\n".join(sqls) else: msg += " (add -v option to show queries)" pytest.fail(msg) diff --git a/pytest_django/live_server_helper.py b/pytest_django/live_server_helper.py index 94ded3315..f61034900 100644 --- a/pytest_django/live_server_helper.py +++ b/pytest_django/live_server_helper.py @@ -1,8 +1,4 @@ -import six - - -@six.python_2_unicode_compatible -class LiveServer(object): +class LiveServer: """The liveserver fixture This is the object that the ``live_server`` fixture returns. @@ -10,7 +6,6 @@ class LiveServer(object): """ def __init__(self, addr): - import django from django.db import connections from django.test.testcases import LiveServerThread from django.test.utils import modify_settings @@ -39,17 +34,13 @@ def __init__(self, addr): liveserver_kwargs["static_handler"] = _StaticFilesHandler - if django.VERSION < (1, 11): - host, possible_ports = parse_addr(addr) - self.thread = LiveServerThread(host, possible_ports, **liveserver_kwargs) + try: + host, port = addr.split(":") + except ValueError: + host = addr else: - try: - host, port = addr.split(":") - except ValueError: - host = addr - else: - liveserver_kwargs["port"] = int(port) - self.thread = LiveServerThread(host, **liveserver_kwargs) + liveserver_kwargs["port"] = int(port) + self.thread = LiveServerThread(host, **liveserver_kwargs) self._live_server_modified_settings = modify_settings( ALLOWED_HOSTS={"append": host} @@ -69,41 +60,13 @@ def stop(self): @property def url(self): - return "http://%s:%s" % (self.thread.host, self.thread.port) + return "http://{}:{}".format(self.thread.host, self.thread.port) def __str__(self): return self.url def __add__(self, other): - return "%s%s" % (self, other) + return "{}{}".format(self, other) def __repr__(self): return "" % self.url - - -def parse_addr(specified_address): - """Parse the --liveserver argument into a host/IP address and port range""" - # This code is based on - # django.test.testcases.LiveServerTestCase.setUpClass - - # The specified ports may be of the form '8000-8010,8080,9200-9300' - # i.e. a comma-separated list of ports or ranges of ports, so we break - # it down into a detailed list of all possible ports. - possible_ports = [] - try: - host, port_ranges = specified_address.split(":") - for port_range in port_ranges.split(","): - # A port range can be of either form: '8000' or '8000-8010'. - extremes = list(map(int, port_range.split("-"))) - assert len(extremes) in (1, 2) - if len(extremes) == 1: - # Port range of the form '8000' - possible_ports.append(extremes[0]) - else: - # Port range of the form '8000-8010' - for port in range(extremes[0], extremes[1] + 1): - possible_ports.append(port) - except Exception: - raise Exception('Invalid address ("%s") for live server.' % specified_address) - - return host, possible_ports diff --git a/pytest_django/migrations.py b/pytest_django/migrations.py index 3c39cb9f6..efcabf929 100644 --- a/pytest_django/migrations.py +++ b/pytest_django/migrations.py @@ -2,7 +2,7 @@ from pytest_django.lazy_django import get_django_version -class DisableMigrations(object): +class DisableMigrations: def __init__(self): self._django_version = get_django_version() @@ -10,7 +10,4 @@ def __contains__(self, item): return True def __getitem__(self, item): - if self._django_version >= (1, 9): - return None - else: - return "notmigrations" + return None diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 4d692de27..a13b2b81a 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -8,6 +8,7 @@ import inspect from functools import reduce import os +import pathlib import sys import types @@ -39,22 +40,11 @@ from .lazy_django import django_settings_is_configured, skip_if_no_django -try: - import pathlib -except ImportError: - import pathlib2 as pathlib - SETTINGS_MODULE_ENV = "DJANGO_SETTINGS_MODULE" CONFIGURATION_ENV = "DJANGO_CONFIGURATION" INVALID_TEMPLATE_VARS_ENV = "FAIL_INVALID_TEMPLATE_VARS" -PY2 = sys.version_info[0] == 2 - -# pytest 4.2 handles unittest setup/teardown itself via wrapping fixtures. -_pytest_version_info = tuple(int(x) for x in pytest.__version__.split(".", 2)[:2]) -_handle_unittest_methods = _pytest_version_info < (4, 2) - _report_header = [] @@ -303,11 +293,11 @@ def _get_option_with_source(option, envname): dc, dc_source = _get_option_with_source(options.dc, CONFIGURATION_ENV) if ds: - _report_header.append("settings: %s (from %s)" % (ds, ds_source)) + _report_header.append("settings: {} (from {})".format(ds, ds_source)) os.environ[SETTINGS_MODULE_ENV] = ds if dc: - _report_header.append("configuration: %s (from %s)" % (dc, dc_source)) + _report_header.append("configuration: {} (from {})".format(dc, dc_source)) os.environ[CONFIGURATION_ENV] = dc # Install the django-configurations importer @@ -330,7 +320,7 @@ def pytest_report_header(): return ["django: " + ", ".join(_report_header)] -@pytest.mark.trylast +@pytest.hookimpl(trylast=True) def pytest_configure(): # Allow Django settings to be configured in a user pytest_configure call, # but make sure we call django.setup() @@ -354,13 +344,7 @@ def _classmethod_is_defined_at_leaf(cls, method_name): try: f = method.__func__ except AttributeError: - pytest.fail("%s.%s should be a classmethod" % (cls, method_name)) - if PY2 and not ( - inspect.ismethod(method) - and inspect.isclass(method.__self__) - and issubclass(cls, method.__self__) - ): - pytest.fail("%s.%s should be a classmethod" % (cls, method_name)) + pytest.fail("{}.{} should be a classmethod".format(cls, method_name)) return f is not super_method.__func__ @@ -409,12 +393,6 @@ def _restore_class_methods(cls): cls.tearDownClass = tearDownClass -def pytest_runtest_setup(item): - if _handle_unittest_methods: - if django_settings_is_configured() and is_django_unittest(item): - _disable_class_methods(item.cls) - - @pytest.hookimpl(tryfirst=True) def pytest_collection_modifyitems(items): # If Django is not configured we don't need to bother @@ -523,33 +501,21 @@ def _django_setup_unittest(request, django_db_blocker): # Fix/patch pytest. # Before pytest 5.4: https://github.com/pytest-dev/pytest/issues/5991 # After pytest 5.4: https://github.com/pytest-dev/pytest-django/issues/824 - from _pytest.monkeypatch import MonkeyPatch + from _pytest.unittest import TestCaseFunction + original_runtest = TestCaseFunction.runtest def non_debugging_runtest(self): self._testcase(result=self) - mp_debug = MonkeyPatch() - mp_debug.setattr("_pytest.unittest.TestCaseFunction.runtest", non_debugging_runtest) - - request.getfixturevalue("django_db_setup") - - cls = request.node.cls - - with django_db_blocker.unblock(): - if _handle_unittest_methods: - _restore_class_methods(cls) - cls.setUpClass() - _disable_class_methods(cls) + try: + TestCaseFunction.runtest = non_debugging_runtest - yield + request.getfixturevalue("django_db_setup") - _restore_class_methods(cls) - cls.tearDownClass() - else: + with django_db_blocker.unblock(): yield - - if mp_debug: - mp_debug.undo() + finally: + TestCaseFunction.runtest = original_runtest @pytest.fixture(scope="function", autouse=True) @@ -591,12 +557,7 @@ def _django_set_urlconf(request): if marker: skip_if_no_django() import django.conf - - try: - from django.urls import clear_url_caches, set_urlconf - except ImportError: - # Removed in Django 2.0 - from django.core.urlresolvers import clear_url_caches, set_urlconf + from django.urls import clear_url_caches, set_urlconf urls = validate_urls(marker) original_urlconf = django.conf.settings.ROOT_URLCONF @@ -629,7 +590,7 @@ def _fail_for_invalid_template_variable(): ``pytest.mark.ignore_template_errors`` """ - class InvalidVarException(object): + class InvalidVarException: """Custom handler for invalid strings in templates.""" def __init__(self): @@ -677,7 +638,7 @@ def __mod__(self, var): """Handle TEMPLATE_STRING_IF_INVALID % var.""" origin = self._get_origin() if origin: - msg = "Undefined template variable '%s' in '%s'" % (var, origin) + msg = "Undefined template variable '{}' in '{}'".format(var, origin) else: msg = "Undefined template variable '%s'" % var if self.fail: @@ -732,7 +693,7 @@ def _django_clear_site_cache(): # ############### Helper Functions ################ -class _DatabaseBlockerContextManager(object): +class _DatabaseBlockerContextManager: def __init__(self, db_blocker): self._db_blocker = db_blocker @@ -743,7 +704,7 @@ def __exit__(self, exc_type, exc_value, traceback): self._db_blocker.restore() -class _DatabaseBlocker(object): +class _DatabaseBlocker: """Manager for django.db.backends.base.base.BaseDatabaseWrapper. This is the object returned by django_db_blocker. diff --git a/pytest_django_test/app/migrations/0001_initial.py b/pytest_django_test/app/migrations/0001_initial.py index 7791cafcf..3a853e557 100644 --- a/pytest_django_test/app/migrations/0001_initial.py +++ b/pytest_django_test/app/migrations/0001_initial.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.9a1 on 2016-06-22 04:33 -from __future__ import unicode_literals from django.db import migrations, models diff --git a/pytest_django_test/compat.py b/pytest_django_test/compat.py deleted file mode 100644 index 0c9ce91f1..000000000 --- a/pytest_django_test/compat.py +++ /dev/null @@ -1,4 +0,0 @@ -try: - from urllib2 import urlopen, HTTPError -except ImportError: - from urllib.request import urlopen, HTTPError # noqa: F401 diff --git a/pytest_django_test/db_helpers.py b/pytest_django_test/db_helpers.py index 29f095711..5e927df47 100644 --- a/pytest_django_test/db_helpers.py +++ b/pytest_django_test/db_helpers.py @@ -31,7 +31,7 @@ def get_db_engine(): return _settings["ENGINE"].split(".")[-1] -class CmdResult(object): +class CmdResult: def __init__(self, status_code, std_out, std_err): self.status_code = status_code self.std_out = std_out @@ -64,7 +64,7 @@ def skip_if_sqlite_in_memory(): def _get_db_name(db_suffix=None): name = TEST_DB_NAME if db_suffix: - name = "%s_%s" % (name, db_suffix) + name = "{}_{}".format(name, db_suffix) return name @@ -72,7 +72,7 @@ def drop_database(db_suffix=None): name = _get_db_name(db_suffix) db_engine = get_db_engine() - if db_engine == "postgresql_psycopg2": + if db_engine == "postgresql": r = run_cmd("psql", "postgres", "-c", "DROP DATABASE %s" % name) assert "DROP DATABASE" in force_str( r.std_out @@ -94,7 +94,7 @@ def db_exists(db_suffix=None): name = _get_db_name(db_suffix) db_engine = get_db_engine() - if db_engine == "postgresql_psycopg2": + if db_engine == "postgresql": r = run_cmd("psql", name, "-c", "SELECT 1") return r.status_code == 0 @@ -111,7 +111,7 @@ def db_exists(db_suffix=None): def mark_database(): db_engine = get_db_engine() - if db_engine == "postgresql_psycopg2": + if db_engine == "postgresql": r = run_cmd("psql", TEST_DB_NAME, "-c", "CREATE TABLE mark_table();") assert r.status_code == 0 return @@ -136,7 +136,7 @@ def mark_database(): def mark_exists(): db_engine = get_db_engine() - if db_engine == "postgresql_psycopg2": + if db_engine == "postgresql": r = run_cmd("psql", TEST_DB_NAME, "-c", "SELECT 1 FROM mark_table") # When something pops out on std_out, we are good diff --git a/pytest_django_test/settings_base.py b/pytest_django_test/settings_base.py index 050386299..4c9b456f9 100644 --- a/pytest_django_test/settings_base.py +++ b/pytest_django_test/settings_base.py @@ -1,5 +1,3 @@ -import django - ROOT_URLCONF = "pytest_django_test.urls" INSTALLED_APPS = [ "django.contrib.auth", @@ -20,9 +18,6 @@ "django.contrib.messages.middleware.MessageMiddleware", ] -if django.VERSION < (1, 10): - MIDDLEWARE_CLASSES = MIDDLEWARE - TEMPLATES = [ { diff --git a/pytest_django_test/settings_postgres.py b/pytest_django_test/settings_postgres.py index 2598beec9..a926438b6 100644 --- a/pytest_django_test/settings_postgres.py +++ b/pytest_django_test/settings_postgres.py @@ -2,7 +2,7 @@ # PyPy compatibility try: - from psycopg2ct import compat + from psycopg2cffi import compat compat.register() except ImportError: @@ -11,9 +11,7 @@ DATABASES = { "default": { - "ENGINE": "django.db.backends.postgresql_psycopg2", + "ENGINE": "django.db.backends.postgresql", "NAME": "pytest_django_should_never_get_accessed", - "HOST": "localhost", - "USER": "", } } diff --git a/pytest_django_test/urls.py b/pytest_django_test/urls.py index e96a371df..363b979c5 100644 --- a/pytest_django_test/urls.py +++ b/pytest_django_test/urls.py @@ -1,8 +1,8 @@ -from django.conf.urls import url +from django.urls import path from .app import views urlpatterns = [ - url(r"^item_count/$", views.item_count), - url(r"^admin-required/$", views.admin_required_view), + path("item_count/", views.item_count), + path("admin-required/", views.admin_required_view), ] diff --git a/pytest_django_test/urls_overridden.py b/pytest_django_test/urls_overridden.py index eca54e663..255a2ca9a 100644 --- a/pytest_django_test/urls_overridden.py +++ b/pytest_django_test/urls_overridden.py @@ -1,6 +1,6 @@ -from django.conf.urls import url +from django.urls import path from django.http import HttpResponse urlpatterns = [ - url(r"^overridden_url/$", lambda r: HttpResponse("Overridden urlconf works!")) + path("overridden_url/", lambda r: HttpResponse("Overridden urlconf works!")) ] diff --git a/setup.py b/setup.py index 8513c299a..361649fc4 100755 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- import codecs import os @@ -28,11 +27,10 @@ def read(fname): license='BSD-3-Clause', packages=['pytest_django'], long_description=read('README.rst'), - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', + python_requires='>=3.5', setup_requires=['setuptools_scm>=1.11.1'], install_requires=[ - 'pytest>=3.6', - 'pathlib2;python_version<"3.4"', + 'pytest>=5.4.0', ], extras_require={ 'docs': [ @@ -42,17 +40,10 @@ def read(fname): 'testing': [ 'Django', 'django-configurations>=2.0', - 'six', ], }, classifiers=['Development Status :: 5 - Production/Stable', 'Framework :: Django', - 'Framework :: Django :: 1.8', - 'Framework :: Django :: 1.9', - 'Framework :: Django :: 1.10', - 'Framework :: Django :: 1.11', - 'Framework :: Django :: 2.0', - 'Framework :: Django :: 2.1', 'Framework :: Django :: 2.2', 'Framework :: Django :: 3.0', 'Framework :: Django :: 3.1', @@ -60,8 +51,6 @@ def read(fname): 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', diff --git a/tests/conftest.py b/tests/conftest.py index 8b76aba29..8481c8dfc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,6 @@ from textwrap import dedent import pytest -import six from django.conf import settings try: @@ -58,7 +57,7 @@ def django_testdir(request, testdir, monkeypatch): # Pypy compatibility try: - from psycopg2ct import compat + from psycopg2cffi import compat except ImportError: pass else: @@ -81,9 +80,6 @@ def django_testdir(request, testdir, monkeypatch): 'django.contrib.messages.middleware.MessageMiddleware', ] - if django.VERSION < (1, 10): - MIDDLEWARE_CLASSES = MIDDLEWARE - TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', @@ -118,7 +114,7 @@ def django_testdir(request, testdir, monkeypatch): test_app_path = tpkg_path.join("app") # Copy the test app to make it available in the new test run - shutil.copytree(six.text_type(app_source), six.text_type(test_app_path)) + shutil.copytree(str(app_source), str(test_app_path)) tpkg_path.join("the_settings.py").write(test_settings) monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "tpkg.the_settings") diff --git a/tests/test_database.py b/tests/test_database.py index 7bcb06289..4b5a0a5d5 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -1,5 +1,3 @@ -from __future__ import with_statement - import pytest from django.db import connection from django.test.testcases import connections_support_transactions diff --git a/tests/test_db_setup.py b/tests/test_db_setup.py index 375cb8449..6499b1019 100644 --- a/tests/test_db_setup.py +++ b/tests/test_db_setup.py @@ -1,6 +1,5 @@ import pytest -from pytest_django.lazy_django import get_django_version from pytest_django_test.db_helpers import ( db_exists, drop_database, @@ -453,33 +452,7 @@ def test_a(): result.stdout.fnmatch_lines(["*PASSED*test_a*"]) -@pytest.mark.skipif( - get_django_version() >= (1, 9), - reason=( - "Django 1.9 requires migration and has no concept of initial data fixtures" - ), -) -def test_initial_data(django_testdir_initial): - """Test that initial data gets loaded.""" - django_testdir_initial.create_test_module( - """ - import pytest - - from .app.models import Item - - @pytest.mark.django_db - def test_inner(): - assert [x.name for x in Item.objects.all()] \ - == ["mark_initial_data"] - """ - ) - - result = django_testdir_initial.runpytest_subprocess("--tb=short", "-v") - assert result.ret == 0 - result.stdout.fnmatch_lines(["*test_inner*PASSED*"]) - - -class TestNativeMigrations(object): +class TestNativeMigrations: """ Tests for Django Migrations """ def test_no_migrations(self, django_testdir): diff --git a/tests/test_django_settings_module.py b/tests/test_django_settings_module.py index f7c0a5d83..685937cce 100644 --- a/tests/test_django_settings_module.py +++ b/tests/test_django_settings_module.py @@ -3,7 +3,6 @@ If these tests fail you probably forgot to run "python setup.py develop". """ -import django import pytest @@ -308,10 +307,6 @@ def test_debug_is_false(): assert r.ret == 0 -@pytest.mark.skipif( - not hasattr(django, "setup"), - reason="This Django version does not support app loading", -) @pytest.mark.django_project( extra_settings=""" INSTALLED_APPS = [ @@ -329,10 +324,7 @@ class TestApp(AppConfig): name = 'tpkg.app' def ready(self): - try: - populating = apps.loading - except AttributeError: # Django < 2.0 - populating = apps._lock.locked() + populating = apps.loading print('READY(): populating=%r' % populating) """, "apps.py", @@ -342,10 +334,7 @@ def ready(self): """ from django.apps import apps - try: - populating = apps.loading - except AttributeError: # Django < 2.0 - populating = apps._lock.locked() + populating = apps.loading print('IMPORT: populating=%r,ready=%r' % (populating, apps.ready)) SOME_THING = 1234 @@ -360,10 +349,7 @@ def ready(self): from tpkg.app.models import SOME_THING def test_anything(): - try: - populating = apps.loading - except AttributeError: # Django < 2.0 - populating = apps._lock.locked() + populating = apps.loading print('TEST: populating=%r,ready=%r' % (populating, apps.ready)) """ @@ -372,10 +358,7 @@ def test_anything(): result = django_testdir.runpytest_subprocess("-s", "--tb=line") result.stdout.fnmatch_lines(["*IMPORT: populating=True,ready=False*"]) result.stdout.fnmatch_lines(["*READY(): populating=True*"]) - if django.VERSION < (2, 0): - result.stdout.fnmatch_lines(["*TEST: populating=False,ready=True*"]) - else: - result.stdout.fnmatch_lines(["*TEST: populating=True,ready=True*"]) + result.stdout.fnmatch_lines(["*TEST: populating=True,ready=True*"]) assert result.ret == 0 diff --git a/tests/test_environment.py b/tests/test_environment.py index 95f903a1b..87e45f5ff 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -1,5 +1,3 @@ -from __future__ import with_statement - import os import pytest @@ -8,7 +6,6 @@ from django.core import mail from django.db import connection from django.test import TestCase -from pytest_django.lazy_django import get_django_version from pytest_django_test.app.models import Item @@ -57,11 +54,11 @@ def test_two(self): def test_invalid_template_variable(django_testdir): django_testdir.create_app_file( """ - from django.conf.urls import url + from django.urls import path from tpkg.app import views - urlpatterns = [url(r'invalid_template/', views.invalid_template)] + urlpatterns = [path('invalid_template/', views.invalid_template)] """, "urls.py", ) @@ -95,10 +92,7 @@ def test_ignore(client): ) result = django_testdir.runpytest_subprocess("-s", "--fail-on-template-vars") - if get_django_version() >= (1, 9): - origin = "'*/tpkg/app/templates/invalid_template_base.html'" - else: - origin = "'invalid_template.html'" + origin = "'*/tpkg/app/templates/invalid_template_base.html'" result.stdout.fnmatch_lines_random( [ "tpkg/test_the_test.py F.*", @@ -163,11 +157,11 @@ def test_for_invalid_template(): def test_invalid_template_variable_opt_in(django_testdir): django_testdir.create_app_file( """ - from django.conf.urls import url + from django.urls import path from tpkg.app import views - urlpatterns = [url(r'invalid_template/', views.invalid_template)] + urlpatterns = [path('invalid_template', views.invalid_template)] """, "urls.py", ) @@ -255,14 +249,9 @@ def test_verbose_with_v(self, testdir): """Verbose output with '-v'.""" result = testdir.runpytest_subprocess("-s", "-v") result.stdout.fnmatch_lines_random(["tpkg/test_the_test.py:*", "*PASSED*"]) - if get_django_version() >= (2, 2): - result.stderr.fnmatch_lines( - ["*Destroying test database for alias 'default'*"] - ) - else: - result.stdout.fnmatch_lines( - ["*Destroying test database for alias 'default'...*"] - ) + result.stderr.fnmatch_lines( + ["*Destroying test database for alias 'default'*"] + ) def test_more_verbose_with_vv(self, testdir): """More verbose output with '-v -v'.""" @@ -275,37 +264,22 @@ def test_more_verbose_with_vv(self, testdir): "*PASSED*", ] ) - if get_django_version() >= (2, 2): - result.stderr.fnmatch_lines( - [ - "*Creating test database for alias*", - "*Destroying test database for alias 'default'*", - ] - ) - else: - result.stdout.fnmatch_lines( - [ - "*Creating test database for alias*", - "*Destroying test database for alias 'default'*", - ] - ) + result.stderr.fnmatch_lines( + [ + "*Creating test database for alias*", + "*Destroying test database for alias 'default'*", + ] + ) def test_more_verbose_with_vv_and_reusedb(self, testdir): """More verbose output with '-v -v', and --create-db.""" result = testdir.runpytest_subprocess("-s", "-v", "-v", "--create-db") result.stdout.fnmatch_lines(["tpkg/test_the_test.py:*", "*PASSED*"]) - if get_django_version() >= (2, 2): - result.stderr.fnmatch_lines(["*Creating test database for alias*"]) - assert ( - "*Destroying test database for alias 'default' ('*')...*" - not in result.stderr.str() - ) - else: - result.stdout.fnmatch_lines(["*Creating test database for alias*"]) - assert ( - "*Destroying test database for alias 'default' ('*')...*" - not in result.stdout.str() - ) + result.stderr.fnmatch_lines(["*Creating test database for alias*"]) + assert ( + "*Destroying test database for alias 'default' ('*')...*" + not in result.stderr.str() + ) @pytest.mark.django_db diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index fae054350..0a25f771f 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -4,10 +4,10 @@ fixtures are tested in test_database. """ -from __future__ import with_statement import socket from contextlib import contextmanager +from urllib.request import urlopen, HTTPError import pytest from django.conf import settings as real_settings @@ -17,9 +17,7 @@ from django.test.testcases import connections_support_transactions from django.utils.encoding import force_str -from pytest_django.lazy_django import get_django_version from pytest_django_test.app.models import Item -from pytest_django_test.compat import HTTPError, urlopen @contextmanager @@ -322,7 +320,7 @@ def test_settings_before(self): from django.conf import settings assert ( - "%s.%s" % (settings.__class__.__module__, settings.__class__.__name__) + "{}.{}".format(settings.__class__.__module__, settings.__class__.__name__) == "django.conf.Settings" ) TestLiveServer._test_settings_before_run = True @@ -335,18 +333,14 @@ def test_change_settings(self, live_server, settings): def test_settings_restored(self): """Ensure that settings are restored after test_settings_before.""" - import django from django.conf import settings assert TestLiveServer._test_settings_before_run is True assert ( - "%s.%s" % (settings.__class__.__module__, settings.__class__.__name__) + "{}.{}".format(settings.__class__.__module__, settings.__class__.__name__) == "django.conf.Settings" ) - if django.VERSION >= (1, 11): - assert settings.ALLOWED_HOSTS == ["testserver"] - else: - assert settings.ALLOWED_HOSTS == ["*"] + assert settings.ALLOWED_HOSTS == ["testserver"] def test_transactions(self, live_server): if not connections_support_transactions(): @@ -417,12 +411,9 @@ def test_serve_static_with_staticfiles_app(self, django_testdir, settings): """ django_testdir.create_test_module( """ - from django.utils.encoding import force_str + from urllib.request import urlopen - try: - from urllib2 import urlopen - except ImportError: - from urllib.request import urlopen + from django.utils.encoding import force_str class TestLiveServer: def test_a(self, live_server, settings): @@ -445,28 +436,6 @@ def test_serve_static_dj17_without_staticfiles_app(self, live_server, settings): with pytest.raises(HTTPError): urlopen(live_server + "/static/a_file.txt").read() - @pytest.mark.skipif( - get_django_version() < (1, 11), reason="Django >= 1.11 required" - ) - def test_specified_port_range_error_message_django_111(self, django_testdir): - django_testdir.create_test_module( - """ - def test_with_live_server(live_server): - pass - """ - ) - - result = django_testdir.runpytest_subprocess("--liveserver=localhost:1234-2345") - result.stdout.fnmatch_lines( - [ - "*Specifying multiple live server ports is not supported in Django 1.11. This " - "will be an error in a future pytest-django release.*" - ] - ) - - @pytest.mark.skipif( - get_django_version() < (1, 11, 2), reason="Django >= 1.11.2 required" - ) def test_specified_port_django_111(self, django_testdir): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: @@ -516,16 +485,11 @@ class MyCustomUser(AbstractUser): ) django_testdir.create_app_file( """ - from tpkg.app import views + from django.urls import path - try: - from django.urls import path - except ImportError: - from django.conf.urls import url + from tpkg.app import views - urlpatterns = [url(r'admin-required/', views.admin_required_view)] - else: - urlpatterns = [path('admin-required/', views.admin_required_view)] + urlpatterns = [path('admin-required/', views.admin_required_view)] """, "urls.py", ) @@ -556,9 +520,6 @@ def test_custom_user_model(admin_client): django_testdir.create_app_file("", "migrations/__init__.py") django_testdir.create_app_file( """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations import django.utils.timezone import django.core.validators diff --git a/tests/test_unittest.py b/tests/test_unittest.py index 1f6dcd55c..95ea3eb3c 100644 --- a/tests/test_unittest.py +++ b/tests/test_unittest.py @@ -1,7 +1,6 @@ import pytest from django.test import TestCase -from pytest_django.plugin import _pytest_version_info from pytest_django_test.app.models import Item @@ -161,16 +160,8 @@ def test_pass(self): result = django_testdir.runpytest_subprocess("-v", "-s") expected_lines = [ "* ERROR at setup of TestFoo.test_pass *", + "E * TypeError: *", ] - if _pytest_version_info < (4, 2): - expected_lines += [ - "E *Failed: .setUpClass should be a classmethod", # noqa:E501 - ] - else: - expected_lines += [ - "E * TypeError: *", - ] - result.stdout.fnmatch_lines(expected_lines) assert result.ret == 1 @@ -217,7 +208,7 @@ def test_setUpClass_mixin(self, django_testdir): """ from django.test import TestCase - class TheMixin(object): + class TheMixin: @classmethod def setUpClass(cls): super(TheMixin, cls).setUpClass() @@ -289,7 +280,7 @@ def test_multi_inheritance_setUpClass(self, django_testdir): # Using a mixin is a regression test, see #280 for more details: # https://github.com/pytest-dev/pytest-django/issues/280 - class SomeMixin(object): + class SomeMixin: pass class TestA(SomeMixin, TestCase): diff --git a/tests/test_urls.py b/tests/test_urls.py index 9001fddce..945540593 100644 --- a/tests/test_urls.py +++ b/tests/test_urls.py @@ -1,14 +1,11 @@ import pytest from django.conf import settings +from django.urls import is_valid_path from django.utils.encoding import force_str @pytest.mark.urls("pytest_django_test.urls_overridden") def test_urls(): - try: - from django.urls import is_valid_path - except ImportError: - from django.core.urlresolvers import is_valid_path assert settings.ROOT_URLCONF == "pytest_django_test.urls_overridden" assert is_valid_path("/overridden_url/") @@ -22,21 +19,18 @@ def test_urls_client(client): def test_urls_cache_is_cleared(testdir): testdir.makepyfile( myurls=""" - from django.conf.urls import url + from django.urls import path def fake_view(request): pass - urlpatterns = [url(r'first/$', fake_view, name='first')] + urlpatterns = [path('first', fake_view, name='first')] """ ) testdir.makepyfile( """ - try: - from django.urls import reverse, NoReverseMatch - except ImportError: # Django < 2.0 - from django.core.urlresolvers import reverse, NoReverseMatch + from django.urls import reverse, NoReverseMatch import pytest @pytest.mark.urls('myurls') @@ -58,32 +52,29 @@ def test_something_else(): def test_urls_cache_is_cleared_and_new_urls_can_be_assigned(testdir): testdir.makepyfile( myurls=""" - from django.conf.urls import url + from django.urls import path def fake_view(request): pass - urlpatterns = [url(r'first/$', fake_view, name='first')] + urlpatterns = [path('first', fake_view, name='first')] """ ) testdir.makepyfile( myurls2=""" - from django.conf.urls import url + from django.urls import path def fake_view(request): pass - urlpatterns = [url(r'second/$', fake_view, name='second')] + urlpatterns = [path('second', fake_view, name='second')] """ ) testdir.makepyfile( """ - try: - from django.urls import reverse, NoReverseMatch - except ImportError: # Django < 2.0 - from django.core.urlresolvers import reverse, NoReverseMatch + from django.urls import reverse, NoReverseMatch import pytest @pytest.mark.urls('myurls') diff --git a/tox.ini b/tox.ini index d6fb991e4..c7ffafaf5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,36 +1,27 @@ [tox] envlist = - py37-dj{31,30,22,21,20,111}-postgres - py36-dj{31,30,22,21,20,111,110,19,18}-postgres - py35-dj{22,21,20,111,110,19,18}-postgres - py34-dj{20,111,110}-postgres - py27-dj{111,110}-{mysql_innodb,mysql_myisam,postgres} - py27-dj{111,110,19,18}-postgres + py38-dj{31,30,22}-postgres + py37-dj{31,30,22}-postgres + py36-dj{31,30,22}-postgres + py35-dj{22}-postgres checkqa [testenv] extras = testing deps = djmaster: https://github.com/django/django/archive/master.tar.gz - dj31: Django>=3.1rc1,<3.2 + dj31: Django>=3.1,<3.2 dj30: Django>=3.0,<3.1 dj22: Django>=2.2,<2.3 - dj21: Django>=2.1,<2.2 - dj20: Django>=2.0,<2.1 - dj111: Django>=1.11,<1.12 - dj110: Django>=1.10,<1.11 - dj19: Django>=1.9,<1.10 - dj18: Django>=1.8,<1.9 mysql_myisam: mysqlclient==1.4.2.post1 mysql_innodb: mysqlclient==1.4.2.post1 postgres: psycopg2-binary + postgres: psycopg2cffi coverage: coverage-enable-subprocess - pytest41: pytest>=4.1,<4.2 - pytest41: attrs==17.4.0 - pytest53: pytest>=5.3,<5.4 + pytest54: pytest>=5.4,<5.5 xdist: pytest-xdist>=1.15 setenv = @@ -66,7 +57,7 @@ commands = [testenv:doc8] extras = -basepython = python3.6 +basepython = python3.8 skip_install = true deps = sphinx @@ -81,7 +72,7 @@ commands = sphinx-build -n -W -b html -d docs/_build/doctrees docs docs/_build/h [testenv:readme] extras = -basepython = python3.5 +basepython = python3.8 deps = readme_renderer commands = From 8c95555ad126e7ab2021f81e5cbff70f4048b44b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 7 Oct 2020 22:45:36 +0300 Subject: [PATCH 020/150] tests: fix failed matching due to django-configuration deprecation warnings --- tests/test_db_access_in_repr.py | 2 +- tests/test_db_setup.py | 2 +- tests/test_django_configurations.py | 8 ++++---- tests/test_django_settings_module.py | 12 ++++++------ tests/test_fixtures.py | 2 +- tests/test_initialization.py | 2 +- tests/test_unittest.py | 6 +++--- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/test_db_access_in_repr.py b/tests/test_db_access_in_repr.py index 89158c40e..c8511cf17 100644 --- a/tests/test_db_access_in_repr.py +++ b/tests/test_db_access_in_repr.py @@ -21,7 +21,7 @@ def test_via_db_fixture(db): "tpkg/test_the_test.py:8: ", 'self = *RuntimeError*Database access not allowed*', "E *DoesNotExist: Item matching query does not exist.", - "* 2 failed in *", + "* 2 failed*", ]) assert "INTERNALERROR" not in str(result.stdout) + str(result.stderr) assert result.ret == 1 diff --git a/tests/test_db_setup.py b/tests/test_db_setup.py index 6499b1019..21e065948 100644 --- a/tests/test_db_setup.py +++ b/tests/test_db_setup.py @@ -480,7 +480,7 @@ def test_inner_migrations(): ) assert result.ret == 0 assert "Operations to perform:" not in result.stdout.str() - result.stdout.fnmatch_lines(["*= 1 passed in *"]) + result.stdout.fnmatch_lines(["*= 1 passed*"]) def test_migrations_run(self, django_testdir): testdir = django_testdir diff --git a/tests/test_django_configurations.py b/tests/test_django_configurations.py index 11d9c0ffd..70c0126ab 100644 --- a/tests/test_django_configurations.py +++ b/tests/test_django_configurations.py @@ -42,7 +42,7 @@ def test_settings(): result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines([ 'django: settings: tpkg.settings_env (from env), configuration: MySettings (from env)', - "* 1 passed in*", + "* 1 passed*", ]) assert result.ret == 0 @@ -73,7 +73,7 @@ def test_ds(): result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines([ 'django: settings: tpkg.settings_env (from env), configuration: MySettings (from env)', - "* 1 passed in*", + "* 1 passed*", ]) assert result.ret == 0 @@ -103,7 +103,7 @@ def test_ds(): result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines([ 'django: settings: tpkg.settings_ini (from ini), configuration: MySettings (from ini)', - "* 1 passed in*", + "* 1 passed*", ]) assert result.ret == 0 @@ -135,6 +135,6 @@ def test_ds(): result.stdout.fnmatch_lines([ 'django: settings: tpkg.settings_opt (from option),' ' configuration: MySettings (from option)', - "* 1 passed in*", + "* 1 passed*", ]) assert result.ret == 0 diff --git a/tests/test_django_settings_module.py b/tests/test_django_settings_module.py index 685937cce..da16ff543 100644 --- a/tests/test_django_settings_module.py +++ b/tests/test_django_settings_module.py @@ -39,7 +39,7 @@ def test_ds(): result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines([ "django: settings: tpkg.settings_ini (from ini)", - "*= 1 passed in *", + "*= 1 passed*", ]) assert result.ret == 0 @@ -60,7 +60,7 @@ def test_settings(): result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines([ "django: settings: tpkg.settings_env (from env)", - "*= 1 passed in *", + "*= 1 passed*", ]) @@ -86,7 +86,7 @@ def test_ds(): result = testdir.runpytest_subprocess("--ds=tpkg.settings_opt") result.stdout.fnmatch_lines([ "django: settings: tpkg.settings_opt (from option)", - "*= 1 passed in *", + "*= 1 passed*", ]) @@ -137,7 +137,7 @@ def test_ds_after_user_conftest(testdir, monkeypatch): testdir.makepyfile(settings_after_conftest="SECRET_KEY='secret'") # testdir.makeconftest("import sys; print(sys.path)") result = testdir.runpytest_subprocess("-v") - result.stdout.fnmatch_lines(["* 1 passed in*"]) + result.stdout.fnmatch_lines(["* 1 passed*"]) assert result.ret == 0 @@ -225,7 +225,7 @@ def test_user_count(): """ ) result = testdir.runpython(p) - result.stdout.fnmatch_lines(["* 4 passed in*"]) + result.stdout.fnmatch_lines(["* 4 passed*"]) def test_settings_in_hook(testdir, monkeypatch): @@ -274,7 +274,7 @@ def test_settings(): """ ) result = testdir.runpytest_subprocess() - result.stdout.fnmatch_lines(["* 1 passed in*"]) + result.stdout.fnmatch_lines(["* 1 passed*"]) assert result.ret == 0 diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 0a25f771f..6a8e4208a 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -564,7 +564,7 @@ class Migration(migrations.Migration): ) result = django_testdir.runpytest_subprocess("-s") - result.stdout.fnmatch_lines(["* 1 passed in*"]) + result.stdout.fnmatch_lines(["* 1 passed*"]) assert result.ret == 0 diff --git a/tests/test_initialization.py b/tests/test_initialization.py index 33544bd26..b30c46f51 100644 --- a/tests/test_initialization.py +++ b/tests/test_initialization.py @@ -54,7 +54,7 @@ def test_ds(): "conftest", "pytest_configure: conftest", "pytest_configure: plugin", - "* 1 passed in*", + "* 1 passed*", ] ) assert result.ret == 0 diff --git a/tests/test_unittest.py b/tests/test_unittest.py index 95ea3eb3c..f9c01d9ed 100644 --- a/tests/test_unittest.py +++ b/tests/test_unittest.py @@ -97,7 +97,7 @@ def test_bar(self): '> assert 0, "trigger_error"', "E AssertionError: trigger_error", "E assert 0", - "*= 1 failed, 1 passed in *", + "*= 1 failed, 1 passed*", ] ) assert result.ret == 1 @@ -399,7 +399,7 @@ def test_noop(self): result = django_testdir.runpytest_subprocess("-q", "-s") result.stdout.fnmatch_lines( - ["*FooBarTestCase.setUpClass*", "*test_noop*", "1 passed in*"] + ["*FooBarTestCase.setUpClass*", "*test_noop*", "1 passed*"] ) assert result.ret == 0 @@ -484,5 +484,5 @@ def test_method(self): ) result = django_testdir.runpytest_subprocess("--pdb") - result.stdout.fnmatch_lines(["*= 1 passed in *"]) + result.stdout.fnmatch_lines(["*= 1 passed*"]) assert result.ret == 0 From 07e72896bd259ab2e326938a2d1a8a83ef8b6d70 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 14 Sep 2020 22:55:43 +0300 Subject: [PATCH 021/150] Switch to github actions --- .github/workflows/main.yml | 128 ++++++++++++++++++++ pytest_django_test/db_helpers.py | 45 +++++-- pytest_django_test/settings_mysql_innodb.py | 7 +- pytest_django_test/settings_mysql_myisam.py | 7 +- pytest_django_test/settings_postgres.py | 7 +- tox.ini | 6 +- 6 files changed, 182 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..cf002457c --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,128 @@ +name: main + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + test: + runs-on: ubuntu-20.04 + continue-on-error: ${{ matrix.allow_failure }} + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + + - name: Setup mysql + if: contains(matrix.name, 'mysql') + run: | + sudo systemctl start mysql.service + echo "TEST_DB_USER=root" >> $GITHUB_ENV + echo "TEST_DB_PASSWORD=root" >> $GITHUB_ENV + + - name: Setup postgresql + if: contains(matrix.name, 'postgres') + run: | + sudo systemctl start postgresql.service + sudo -u postgres createuser --createdb $USER + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox==3.20.0 + + - name: Run tox + run: tox -e ${{ matrix.name }} + + - name: Report coverage + if: contains(matrix.name, 'coverage') + run: | + bash <(curl -s https://codecov.io/bash) -Z -X gcov -X xcode -X gcovout + + strategy: + fail-fast: false + matrix: + include: + - name: checkqa,docs + python: 3.8 + allow_failure: false + + - name: py38-dj31-postgres-xdist-coverage + python: 3.8 + allow_failure: false + + - name: py37-dj30-mysql_innodb-coverage + python: 3.7 + allow_failure: false + + - name: py36-dj22-sqlite-xdist-coverage + python: 3.6 + allow_failure: false + + - name: py37-dj22-sqlite-xdist-coverage + python: 3.7 + allow_failure: false + + - name: py38-dj30-sqlite-xdist-coverage + python: 3.8 + allow_failure: false + + - name: py38-dj31-sqlite-xdist-coverage + python: 3.8 + allow_failure: false + + - name: py38-djmaster-sqlite-coverage + python: 3.8 + allow_failure: true + + # Explicitly test (older) pytest 5.4. + - name: py35-dj22-postgres-pytest54-coverage + python: 3.5 + allow_failure: false + + - name: py35-dj22-sqlite_file-coverage + python: 3.5 + allow_failure: false + + - name: py36-dj31-mysql_myisam-coverage + python: 3.6 + allow_failure: false + + # pypy3: not included with coverage reports (much slower then). + - name: pypy3-dj22-postgres + python: pypy3 + allow_failure: false + + deploy: + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pytest-django' + runs-on: ubuntu-20.04 + needs: [test] + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v2 + with: + python-version: "3.8" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install --upgrade wheel setuptools tox + + - name: Build package + run: python setup.py sdist bdist_wheel + + - name: Publish package + uses: pypa/gh-action-pypi-publish@1.4.1 + with: + user: __token__ + password: ${{ secrets.pypi_token }} diff --git a/pytest_django_test/db_helpers.py b/pytest_django_test/db_helpers.py index 5e927df47..d3ec63764 100644 --- a/pytest_django_test/db_helpers.py +++ b/pytest_django_test/db_helpers.py @@ -38,19 +38,44 @@ def __init__(self, status_code, std_out, std_err): self.std_err = std_err -def run_cmd(*args): - r = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) +def run_cmd(*args, env=None): + r = subprocess.Popen( + args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env={**os.environ, **(env or {})}, + ) stdoutdata, stderrdata = r.communicate() ret = r.wait() return CmdResult(ret, stdoutdata, stderrdata) +def run_psql(*args): + env = {} + user = _settings.get("USER") + if user: # pragma: no branch + args = ("-U", user, *args) + password = _settings.get("PASSWORD") + if password: # pragma: no branch + env["PGPASSWORD"] = password + host = _settings.get("HOST") + if host: # pragma: no branch + args = ("-h", host, *args) + return run_cmd("psql", *args, env=env) + + def run_mysql(*args): - user = _settings.get("USER", None) + user = _settings.get("USER") if user: # pragma: no branch - args = ("-u", user) + tuple(args) - args = ("mysql",) + tuple(args) - return run_cmd(*args) + args = ("-u", user, *args) + password = _settings.get("PASSWORD") + if password: # pragma: no branch + # Note: "-ppassword" must be a single argument. + args = ("-p" + password, *args) + host = _settings.get("HOST") + if host: # pragma: no branch + args = ("-h", host, *args) + return run_cmd("mysql", *args) def skip_if_sqlite_in_memory(): @@ -73,7 +98,7 @@ def drop_database(db_suffix=None): db_engine = get_db_engine() if db_engine == "postgresql": - r = run_cmd("psql", "postgres", "-c", "DROP DATABASE %s" % name) + r = run_psql("postgres", "-c", "DROP DATABASE %s" % name) assert "DROP DATABASE" in force_str( r.std_out ) or "does not exist" in force_str(r.std_err) @@ -95,7 +120,7 @@ def db_exists(db_suffix=None): db_engine = get_db_engine() if db_engine == "postgresql": - r = run_cmd("psql", name, "-c", "SELECT 1") + r = run_psql(name, "-c", "SELECT 1") return r.status_code == 0 if db_engine == "mysql": @@ -112,7 +137,7 @@ def mark_database(): db_engine = get_db_engine() if db_engine == "postgresql": - r = run_cmd("psql", TEST_DB_NAME, "-c", "CREATE TABLE mark_table();") + r = run_psql(TEST_DB_NAME, "-c", "CREATE TABLE mark_table();") assert r.status_code == 0 return @@ -137,7 +162,7 @@ def mark_exists(): db_engine = get_db_engine() if db_engine == "postgresql": - r = run_cmd("psql", TEST_DB_NAME, "-c", "SELECT 1 FROM mark_table") + r = run_psql(TEST_DB_NAME, "-c", "SELECT 1 FROM mark_table") # When something pops out on std_out, we are good return bool(r.std_out) diff --git a/pytest_django_test/settings_mysql_innodb.py b/pytest_django_test/settings_mysql_innodb.py index 1fa08885a..5adc36526 100644 --- a/pytest_django_test/settings_mysql_innodb.py +++ b/pytest_django_test/settings_mysql_innodb.py @@ -1,11 +1,14 @@ +from os import environ + from .settings_base import * # noqa: F401 F403 DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": "pytest_django_should_never_get_accessed", - "HOST": "localhost", - "USER": "root", + "USER": environ.get("TEST_DB_USER", "root"), + "PASSWORD": environ.get("TEST_DB_PASSWORD", ""), + "HOST": environ.get("TEST_DB_HOST", "localhost"), "OPTIONS": {"init_command": "SET default_storage_engine=InnoDB"}, } } diff --git a/pytest_django_test/settings_mysql_myisam.py b/pytest_django_test/settings_mysql_myisam.py index d0a89afac..8e9106935 100644 --- a/pytest_django_test/settings_mysql_myisam.py +++ b/pytest_django_test/settings_mysql_myisam.py @@ -1,11 +1,14 @@ +from os import environ + from .settings_base import * # noqa: F401 F403 DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": "pytest_django_should_never_get_accessed", - "HOST": "localhost", - "USER": "root", + "USER": environ.get("TEST_DB_USER", "root"), + "PASSWORD": environ.get("TEST_DB_PASSWORD", ""), + "HOST": environ.get("TEST_DB_HOST", "localhost"), "OPTIONS": {"init_command": "SET default_storage_engine=MyISAM"}, } } diff --git a/pytest_django_test/settings_postgres.py b/pytest_django_test/settings_postgres.py index a926438b6..5c387ef7b 100644 --- a/pytest_django_test/settings_postgres.py +++ b/pytest_django_test/settings_postgres.py @@ -1,3 +1,5 @@ +from os import environ + from .settings_base import * # noqa: F401 F403 # PyPy compatibility @@ -13,5 +15,8 @@ "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "pytest_django_should_never_get_accessed", - } + "USER": environ.get("TEST_DB_USER", ""), + "PASSWORD": environ.get("TEST_DB_PASSWORD", ""), + "HOST": environ.get("TEST_DB_HOST", ""), + }, } diff --git a/tox.ini b/tox.ini index c7ffafaf5..c24da8e9e 100644 --- a/tox.ini +++ b/tox.ini @@ -17,8 +17,8 @@ deps = mysql_myisam: mysqlclient==1.4.2.post1 mysql_innodb: mysqlclient==1.4.2.post1 - postgres: psycopg2-binary - postgres: psycopg2cffi + !pypy3-postgres: psycopg2-binary + pypy3-postgres: psycopg2cffi coverage: coverage-enable-subprocess pytest54: pytest>=5.4,<5.5 @@ -38,7 +38,7 @@ setenv = coverage: COVERAGE_FILE={toxinidir}/.coverage coverage: PYTESTDJANGO_COVERAGE_SRC={toxinidir}/ -passenv = PYTEST_ADDOPTS TERM +passenv = PYTEST_ADDOPTS TERM TEST_DB_USER TEST_DB_PASSWORD TEST_DB_HOST usedevelop = True commands = coverage: coverage erase From 18f78ff480cfa0b65c7b7fa226217f3837159782 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 9 Oct 2020 11:17:56 +0300 Subject: [PATCH 022/150] Comment out coverage & release on travis While testing github actions, we don't want to remove travis entirely yet, but should disable duplicate functionality. --- .travis.yml | 56 ++++++++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/.travis.yml b/.travis.yml index 60ecef3d8..28a8f4600 100644 --- a/.travis.yml +++ b/.travis.yml @@ -53,24 +53,24 @@ jobs: services: - postgresql - - stage: test_release - python: 3.8 - env: TOXENV=py38-dj31-postgres - services: - - postgresql +# - stage: test_release +# python: 3.8 +# env: TOXENV=py38-dj31-postgres +# services: +# - postgresql - - stage: release - script: skip - install: skip - after_success: true - deploy: - provider: pypi - user: blueyed - password: - secure: "FY7qbX/N0XRcH8hVk00SsQWvNIkuxKvY7Br4ghRnHvleHG3YulJ7WbJnik+9eoBGeMfJeNyzBfVjpeo1ZIq9IZBiyTdNfG/sZFsC5LOoG/CPxPH3nD9JktI2HoBMnlSbGg/MMHjY+wXuOY647U/3qNedcnQmGztYt6QWi5DRxu8=" - on: - tags: true - distributions: "sdist bdist_wheel" +# - stage: release +# script: skip +# install: skip +# after_success: true +# deploy: +# provider: pypi +# user: blueyed +# password: +# secure: "FY7qbX/N0XRcH8hVk00SsQWvNIkuxKvY7Br4ghRnHvleHG3YulJ7WbJnik+9eoBGeMfJeNyzBfVjpeo1ZIq9IZBiyTdNfG/sZFsC5LOoG/CPxPH3nD9JktI2HoBMnlSbGg/MMHjY+wXuOY647U/3qNedcnQmGztYt6QWi5DRxu8=" +# on: +# tags: true +# distributions: "sdist bdist_wheel" # NOTE: does not show up in "allowed failures" section, but is allowed to # fail (for the "test" stage). @@ -82,10 +82,10 @@ stages: if: tag IS NOT present - name: test if: tag IS NOT present - - name: test_release - if: tag IS present - - name: release - if: tag IS present + # - name: test_release + # if: tag IS present + # - name: release + # if: tag IS present install: - pip install tox==3.20.0 @@ -93,10 +93,10 @@ install: script: - tox -after_success: - - | - set -ex - if [[ "${TOXENV%-coverage}" != "$TOXENV" ]]; then - bash <(curl -s https://codecov.io/bash) -Z -X gcov -X xcode -X gcovout - fi - set +ex +# after_success: +# - | +# set -ex +# if [[ "${TOXENV%-coverage}" != "$TOXENV" ]]; then +# bash <(curl -s https://codecov.io/bash) -Z -X gcov -X xcode -X gcovout +# fi +# set +ex From f786a0f6977650cce087f2a161d79a19ba5cb28a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 9 Oct 2020 12:43:49 +0300 Subject: [PATCH 023/150] Remove travis CI Switched to github actions. --- .travis.yml | 102 ------------------------------------------ README.rst | 4 +- docs/contributing.rst | 8 ++-- 3 files changed, 6 insertions(+), 108 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 28a8f4600..000000000 --- a/.travis.yml +++ /dev/null @@ -1,102 +0,0 @@ -language: python -dist: focal -cache: false - -jobs: - fast_finish: true - include: - - stage: baseline - python: 3.8 - env: - - TOXENV=py38-dj31-postgres-xdist-coverage - # Test in verbose mode. - - PYTEST_ADDOPTS=-vv - services: - - postgresql - - python: 3.7 - env: TOXENV=py37-dj30-mysql_innodb-coverage - services: - - mysql - - python: 3.6 - env: TOXENV=py36-dj22-sqlite-xdist-coverage - - python: 3.8 - env: TOXENV=checkqa,docs - - - stage: test - python: 3.7 - env: TOXENV=py37-dj22-sqlite-xdist-coverage - - python: 3.8 - env: TOXENV=py38-dj30-sqlite-xdist-coverage - - python: 3.8 - env: TOXENV=py38-dj31-sqlite-xdist-coverage - - - python: 3.8 - env: TOXENV=py38-djmaster-sqlite-coverage - - # Explicitly test (older) pytest 5.4. - - python: 3.5 - env: TOXENV=py35-dj22-postgres-pytest54-coverage - services: - - postgresql - - - python: 3.5 - env: TOXENV=py35-dj22-sqlite_file-coverage - - - python: 3.6 - env: TOXENV=py36-dj31-mysql_myisam-coverage - services: - - mysql - - # pypy3: not included with coverage reports (much slower then). - - python: pypy3 - env: TOXENV=pypy3-dj22-postgres - services: - - postgresql - -# - stage: test_release -# python: 3.8 -# env: TOXENV=py38-dj31-postgres -# services: -# - postgresql - -# - stage: release -# script: skip -# install: skip -# after_success: true -# deploy: -# provider: pypi -# user: blueyed -# password: -# secure: "FY7qbX/N0XRcH8hVk00SsQWvNIkuxKvY7Br4ghRnHvleHG3YulJ7WbJnik+9eoBGeMfJeNyzBfVjpeo1ZIq9IZBiyTdNfG/sZFsC5LOoG/CPxPH3nD9JktI2HoBMnlSbGg/MMHjY+wXuOY647U/3qNedcnQmGztYt6QWi5DRxu8=" -# on: -# tags: true -# distributions: "sdist bdist_wheel" - - # NOTE: does not show up in "allowed failures" section, but is allowed to - # fail (for the "test" stage). - allow_failures: - - env: TOXENV=py38-djmaster-sqlite-coverage - -stages: - - name: baseline - if: tag IS NOT present - - name: test - if: tag IS NOT present - # - name: test_release - # if: tag IS present - # - name: release - # if: tag IS present - -install: - - pip install tox==3.20.0 - -script: - - tox - -# after_success: -# - | -# set -ex -# if [[ "${TOXENV%-coverage}" != "$TOXENV" ]]; then -# bash <(curl -s https://codecov.io/bash) -Z -X gcov -X xcode -X gcovout -# fi -# set +ex diff --git a/README.rst b/README.rst index 367367c63..6d1f1ba3f 100644 --- a/README.rst +++ b/README.rst @@ -6,9 +6,9 @@ :alt: Supported Python versions :target: https://pypi.python.org/pypi/pytest-django -.. image:: https://travis-ci.org/pytest-dev/pytest-django.svg?branch=master +.. image:: https://github.com/pytest-dev/pytest-django/workflows/main/badge.svg :alt: Build Status - :target: https://travis-ci.org/pytest-dev/pytest-django + :target: https://github.com/pytest-dev/pytest-django/actions .. image:: https://img.shields.io/codecov/c/github/pytest-dev/pytest-django.svg?style=flat :alt: Coverage diff --git a/docs/contributing.rst b/docs/contributing.rst index 775e052ed..705cbc5ed 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -178,10 +178,10 @@ Your coverage report is now ready in the ``htmlcov`` directory. Continuous integration ---------------------- -`Travis`_ is used to automatically run all tests against all supported versions +`GitHub Actions`_ is used to automatically run all tests against all supported versions of Python, Django and different database backends. -The `pytest-django Travis`_ page shows the latest test run. Travis will +The `pytest-django Actions`_ page shows the latest test run. The CI will automatically pick up pull requests, test them and report the result directly in the pull request. @@ -237,6 +237,6 @@ double cookie points. Seriously. You rock. .. _git : http://git-scm.com/ .. _restructuredText: http://docutils.sourceforge.net/docs/ref/rst/introduction.html .. _django CMS: https://www.django-cms.org/ -.. _Travis: https://travis-ci.org/ -.. _pytest-django Travis: https://travis-ci.org/pytest-dev/pytest-django +.. _GitHub Actions: https://github.com/features/actions +.. _pytest-django Actions: https://github.com/pytest-dev/pytest-django/actions .. _`subprocess section of coverage documentation`: http://nedbatchelder.com/code/coverage/subprocess.html From d8eb86f1bfcf15955fed9ad4d617ba5ed20a22a1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 9 Oct 2020 13:39:43 +0300 Subject: [PATCH 024/150] Move stuff from setup.py to setup.cfg Using https://github.com/asottile/setup-py-upgrade. Similar to pytest. --- setup.cfg | 57 ++++++++++++++++++++++++++++++++++++++++++++----- setup.py | 64 +------------------------------------------------------ 2 files changed, 53 insertions(+), 68 deletions(-) diff --git a/setup.cfg b/setup.cfg index a26f8c40b..fd2f3b4d9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,54 @@ +[metadata] +name = pytest-django +description = A Django plugin for pytest. +long_description = file: README.rst +long_description_content_type = text/x-rst +author = Andreas Pelme +author_email = andreas@pelme.se +maintainer = Andreas Pelme +maintainer_email = andreas@pelme.se +url = https://pytest-django.readthedocs.io/ +license = BSD-3-Clause +license_file = LICENSE +classifiers = + Development Status :: 5 - Production/Stable + Framework :: Django + Framework :: Django :: 2.2 + Framework :: Django :: 3.0 + Framework :: Django :: 3.1 + Intended Audience :: Developers + License :: OSI Approved :: BSD License + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy + Topic :: Software Development :: Testing +project_urls = + Source=https://github.com/pytest-dev/pytest-django + Changelog=https://pytest-django.readthedocs.io/en/latest/changelog.html + +[options] +packages = pytest_django +python_requires = >=3.5 +setup_requires = setuptools_scm>=1.11.1 +install_requires = pytest>=5.4.0 + +[options.entry_points] +pytest11 = + django = pytest_django.plugin + +[options.extras_require] +docs = + sphinx + sphinx_rtd_theme +testing = + Django + django-configurations>=2.0 + [tool:pytest] # --strict: warnings become errors. # -ra: show extra test summary info for everything. @@ -15,8 +66,4 @@ max-line-length = 99 exclude = lib/,src/,docs/,bin/ [isort] -# NOTE: local imports are handled special (they do not get handled as "tests"). -forced_separate=tests,pytest_django,pytest_django_test - -[metadata] -license_file=LICENSE +forced_separate = tests,pytest_django,pytest_django_test diff --git a/setup.py b/setup.py index 361649fc4..4ca67768e 100755 --- a/setup.py +++ b/setup.py @@ -1,67 +1,5 @@ -#!/usr/bin/env python - -import codecs -import os - from setuptools import setup - -# Utility function to read the README file. -# Used for the long_description. It's nice, because now 1) we have a top level -# README file and 2) it's easier to type in the README file than to put a raw -# string in below ... -def read(fname): - file_path = os.path.join(os.path.dirname(__file__), fname) - return codecs.open(file_path, encoding='utf-8').read() - - setup( - name='pytest-django', use_scm_version=True, - description='A Django plugin for pytest.', - author='Andreas Pelme', - author_email='andreas@pelme.se', - maintainer="Andreas Pelme", - maintainer_email="andreas@pelme.se", - url='https://pytest-django.readthedocs.io/', - license='BSD-3-Clause', - packages=['pytest_django'], - long_description=read('README.rst'), - python_requires='>=3.5', - setup_requires=['setuptools_scm>=1.11.1'], - install_requires=[ - 'pytest>=5.4.0', - ], - extras_require={ - 'docs': [ - 'sphinx', - 'sphinx_rtd_theme', - ], - 'testing': [ - 'Django', - 'django-configurations>=2.0', - ], - }, - classifiers=['Development Status :: 5 - Production/Stable', - 'Framework :: Django', - 'Framework :: Django :: 2.2', - 'Framework :: Django :: 3.0', - 'Framework :: Django :: 3.1', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Topic :: Software Development :: Testing', - ], - project_urls={ - 'Source': 'https://github.com/pytest-dev/pytest-django', - 'Changelog': 'https://pytest-django.readthedocs.io/en/latest/changelog.html', - }, - # the following makes a plugin available to pytest - entry_points={'pytest11': ['django = pytest_django.plugin']}) +) From f0871a9a421708f672d3fa3a1d6e8db965f8ee4f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 9 Oct 2020 13:45:36 +0300 Subject: [PATCH 025/150] Add pytest_django.__version__ This can avoid import importlib-metadata or pkg_resources etc. Similar to pytest. --- .gitignore | 2 ++ pytest_django/__init__.py | 10 ++++++++++ setup.py | 4 +++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 90131acf9..35f1856e7 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ _build .Python .eggs *.egg +# autogenerated by setuptools-scm +/pytest_django/_version.py diff --git a/pytest_django/__init__.py b/pytest_django/__init__.py index e69de29bb..09f9c779e 100644 --- a/pytest_django/__init__.py +++ b/pytest_django/__init__.py @@ -0,0 +1,10 @@ +try: + from ._version import version as __version__ +except ImportError: # pragma: no cover + # Broken installation, we don't even try. + __version__ = "unknown" + + +__all__ = ( + "__version__", +) diff --git a/setup.py b/setup.py index 4ca67768e..abd9cb67f 100755 --- a/setup.py +++ b/setup.py @@ -1,5 +1,7 @@ from setuptools import setup setup( - use_scm_version=True, + use_scm_version={ + 'write_to': 'pytest_django/_version.py', + }, ) From bb9e86e0c0141a30d07078f71b288026b6e583d2 Mon Sep 17 00:00:00 2001 From: Olzhas Arystanov Date: Fri, 9 Oct 2020 18:27:58 +0600 Subject: [PATCH 026/150] admin_user - get user by natural key (#879) --- pytest_django/fixtures.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 0f2dd6115..3a840a57a 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -287,7 +287,11 @@ def admin_user(db, django_user_model, django_username_field): username = "admin@example.com" if username_field == "email" else "admin" try: - user = UserModel._default_manager.get(**{username_field: username}) + # The default behavior of `get_by_natural_key()` is to look up by `username_field`. + # However the user model is free to override it with any sort of custom behavior. + # The Django authentication backend already assumes the lookup is by username, + # so we can assume so as well. + user = UserModel._default_manager.get_by_natural_key(username) except UserModel.DoesNotExist: extra_fields = {} if username_field not in ("username", "email"): From d9b66a2feb9891a584ddd286835ffc93203476d2 Mon Sep 17 00:00:00 2001 From: p-himik Date: Wed, 20 Sep 2017 17:42:07 +0700 Subject: [PATCH 027/150] Force login admin user in admin_client This bypasses the username+password entirely; they cause complications and may fail. [ran: rebased & adjusted a bit] --- docs/helpers.rst | 8 ++++++-- pytest_django/fixtures.py | 2 +- tests/test_fixtures.py | 11 +++++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/helpers.rst b/docs/helpers.rst index 03434faf7..d70ffe2d0 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -158,14 +158,18 @@ Example response = client.get('/') assert response.content == 'Foobar' -To use `client` as an authenticated standard user, call its `login()` method before accessing a URL: +To use `client` as an authenticated standard user, call its `force_login()` or +`login()` method before accessing a URL: :: def test_with_authenticated_client(client, django_user_model): username = "user1" password = "bar" - django_user_model.objects.create_user(username=username, password=password) + user = django_user_model.objects.create_user(username=username, password=password) + # Use this: + client.force_login(user) + # Or this: client.login(username=username, password=password) response = client.get('/private') assert response.content == 'Protected Area' diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 0f2dd6115..d1918d3fd 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -304,7 +304,7 @@ def admin_client(db, admin_user): from django.test.client import Client client = Client() - client.login(username=admin_user.username, password="password") + client.force_login(admin_user) return client diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 6a8e4208a..26b5394aa 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -49,6 +49,17 @@ def test_admin_client_no_db_marker(admin_client): assert force_str(resp.content) == "You are an admin" +# For test below. +@pytest.fixture +def existing_admin_user(django_user_model): + return django_user_model._default_manager.create_superuser('admin', None, None) + + +def test_admin_client_existing_user(db, existing_admin_user, admin_user, admin_client): + resp = admin_client.get("/admin-required/") + assert force_str(resp.content) == "You are an admin" + + @pytest.mark.django_db def test_admin_user(admin_user, django_user_model): assert isinstance(admin_user, django_user_model) From 39ad360a86a5b3c62d60db00e245edadcca78eb5 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 9 Oct 2020 12:56:47 +0300 Subject: [PATCH 028/150] Replace pytest.yield_fixture() with plain pytest.fixture() yield_fixture is a soft-deprecated alias to fixture now. --- docs/database.rst | 2 +- pytest_django/fixtures.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/database.rst b/docs/database.rst index 347312d6c..be5246763 100644 --- a/docs/database.rst +++ b/docs/database.rst @@ -333,7 +333,7 @@ Put this into ``conftest.py``:: conn.close() - @pytest.yield_fixture(scope='session') + @pytest.fixture(scope='session') def django_db_setup(): from django.conf import settings diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 6f6d1d665..df163d661 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -355,7 +355,7 @@ def finalize(self): del self._to_restore[:] -@pytest.yield_fixture() +@pytest.fixture() def settings(): """A Django settings object which restores changes after the testrun""" skip_if_no_django() From 3a6128bd9622139560e1b33e43b1e5224a302f4b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 10 Oct 2020 19:28:34 +0300 Subject: [PATCH 029/150] docs: point to stable versions, improve references in a few places --- docs/conf.py | 6 ++-- docs/configuring_django.rst | 2 +- docs/database.rst | 31 +++++++++----------- docs/faq.rst | 11 ++++--- docs/helpers.rst | 57 ++++++++++++++++--------------------- docs/index.rst | 3 +- docs/tutorial.rst | 2 +- docs/usage.rst | 8 +++--- 8 files changed, 54 insertions(+), 66 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index b049c68ee..5ccc34110 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -40,9 +40,9 @@ intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), - 'django': ('https://docs.djangoproject.com/en/dev/', - 'https://docs.djangoproject.com/en/dev/_objects/'), - 'pytest': ('https://docs.pytest.org/en/latest/', None), + 'django': ('https://docs.djangoproject.com/en/stable/', + 'https://docs.djangoproject.com/en/stable/_objects/'), + 'pytest': ('https://docs.pytest.org/en/stable/', None), } diff --git a/docs/configuring_django.rst b/docs/configuring_django.rst index 21f4debf9..e5aaa1bbf 100644 --- a/docs/configuring_django.rst +++ b/docs/configuring_django.rst @@ -9,7 +9,7 @@ the tests. The environment variable ``DJANGO_SETTINGS_MODULE`` --------------------------------------------------- -Running the tests with DJANGO_SETTINGS_MODULE defined will find the +Running the tests with ``DJANGO_SETTINGS_MODULE`` defined will find the Django settings the same way Django does by default. Example:: diff --git a/docs/database.rst b/docs/database.rst index be5246763..0f00fc95c 100644 --- a/docs/database.rst +++ b/docs/database.rst @@ -12,8 +12,8 @@ what code uses the database and catches any mistakes. Enabling database access in tests --------------------------------- -You can use `pytest marks `_ to -tell ``pytest-django`` your test needs database access:: +You can use :ref:`pytest marks ` to tell ``pytest-django`` your +test needs database access:: import pytest @@ -24,10 +24,8 @@ tell ``pytest-django`` your test needs database access:: It is also possible to mark all tests in a class or module at once. This demonstrates all the ways of marking, even though they overlap. -Just one of these marks would have been sufficient. See the `pytest -documentation -`_ -for detail:: +Just one of these marks would have been sufficient. See the :ref:`pytest +documentation ` for detail:: import pytest @@ -45,20 +43,18 @@ By default ``pytest-django`` will set up the Django databases the first time a test needs them. Once setup the database is cached for used for all subsequent tests and rolls back transactions to isolate tests from each other. This is the same way the standard Django -`TestCase -`_ -uses the database. However ``pytest-django`` also caters for -transaction test cases and allows you to keep the test databases -configured across different test runs. +:class:`~django.test.TestCase` uses the database. However +``pytest-django`` also caters for transaction test cases and allows +you to keep the test databases configured across different test runs. Testing transactions -------------------- -Django itself has the ``TransactionTestCase`` which allows you to test -transactions and will flush the database between tests to isolate -them. The downside of this is that these tests are much slower to -set up due to the required flushing of the database. +Django itself has the :class:`~django.test.TransactionTestCase` which +allows you to test transactions and will flush the database between +tests to isolate them. The downside of this is that these tests are +much slower to set up due to the required flushing of the database. ``pytest-django`` also supports this style of tests, which you can select using an argument to the ``django_db`` mark:: @@ -184,8 +180,9 @@ django_db_modify_db_settings .. fixture:: django_db_modify_db_settings -This fixture allows modifying `django.conf.settings.DATABASES` just before the -databases are configured. +This fixture allows modifying +`django.conf.settings.DATABASES `_ +just before the databases are configured. If you need to customize the location of your test database, this is the fixture you want to override. diff --git a/docs/faq.rst b/docs/faq.rst index 432e47f48..fd08dd810 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -16,9 +16,10 @@ for more information. How can I make sure that all my tests run with a specific locale? ----------------------------------------------------------------- -Create a `pytest fixture `_ that is -automatically run before each test case. To run all tests with the english -locale, put the following code in your project's `conftest.py`_ file: +Create a :ref:`pytest fixture ` that is +automatically run before each test case. To run all tests with the English +locale, put the following code in your project's +:ref:`conftest.py ` file: .. code-block:: python @@ -28,8 +29,6 @@ locale, put the following code in your project's `conftest.py`_ file: def set_default_language(): activate('en') -.. _conftest.py: http://docs.pytest.org/en/latest/plugins.html - .. _faq-tests-not-being-picked-up: My tests are not being found. Why? @@ -55,7 +54,7 @@ When debugging test collection problems, the ``--collectonly`` flag and ``-rs`` (report skipped tests) can be helpful. .. _related pytest docs: - http://docs.pytest.org/en/latest/example/pythoncollection.html#changing-naming-conventions + http://docs.pytest.org/en/stable/example/pythoncollection.html#changing-naming-conventions Does pytest-django work with the pytest-xdist plugin? ----------------------------------------------------- diff --git a/docs/helpers.rst b/docs/helpers.rst index d70ffe2d0..9ee86868e 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -16,11 +16,9 @@ All of Django's :py:class:`~django:django.test.TestCase` Markers ------- -``pytest-django`` registers and uses markers. See the pytest documentation_ -on what marks are and for notes on using_ them. - -.. _documentation: https://pytest.org/en/latest/mark.html -.. _using: https://pytest.org/en/latest/example/markers.html#marking-whole-classes-or-modules +``pytest-django`` registers and uses markers. See the pytest +:ref:`documentation ` on what marks are and for notes on +:ref:`using ` them. ``pytest.mark.django_db`` - request database access @@ -32,7 +30,7 @@ This is used to mark a test function as requiring the database. It will ensure the database is set up correctly for the test. Each test will run in its own transaction which will be rolled back at the end of the test. This behavior is the same as Django's standard -`django.test.TestCase`_ class. +:class:`~django.test.TestCase` class. In order for a test to have access to the database it must either be marked using the ``django_db`` mark or request one of the ``db``, @@ -44,9 +42,8 @@ test will fail when trying to access the database. The ``transaction`` argument will allow the test to use real transactions. With ``transaction=False`` (the default when not specified), transaction operations are noops during the test. This is the same behavior that - `django.test.TestCase`_ - uses. When ``transaction=True``, the behavior will be the same as - `django.test.TransactionTestCase`_ + :class:`django.test.TestCase` uses. When ``transaction=True``, the behavior + will be the same as :class:`django.test.TransactionTestCase`. :type reset_sequences: bool @@ -68,13 +65,10 @@ test will fail when trying to access the database. .. note:: Automatic usage with ``django.test.TestCase``. - Test classes that subclass `django.test.TestCase`_ will have access to + Test classes that subclass :class:`django.test.TestCase` will have access to the database always to make them compatible with existing Django tests. - Test classes that subclass Python's ``unittest.TestCase`` need to have the - marker applied in order to access the database. - -.. _django.test.TestCase: https://docs.djangoproject.com/en/dev/topics/testing/overview/#testcase -.. _django.test.TransactionTestCase: https://docs.djangoproject.com/en/dev/topics/testing/overview/#transactiontestcase + Test classes that subclass Python's :class:`unittest.TestCase` need to have + the marker applied in order to access the database. ``pytest.mark.urls`` - override the urlconf @@ -119,16 +113,14 @@ Fixtures -------- pytest-django provides some pytest fixtures to provide dependencies for tests. -More information on fixtures is available in the `pytest documentation -`_. +More information on fixtures is available in the :ref:`pytest documentation +`. ``rf`` - ``RequestFactory`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -An instance of a `django.test.RequestFactory`_ - -.. _django.test.RequestFactory: https://docs.djangoproject.com/en/dev/topics/testing/advanced/#django.test.RequestFactory +An instance of a :class:`django.test.RequestFactory`. Example """"""" @@ -145,9 +137,7 @@ Example ``client`` - ``django.test.Client`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -An instance of a `django.test.Client`_ - -.. _django.test.Client: https://docs.djangoproject.com/en/dev/topics/testing/tools/#the-test-client +An instance of a :class:`django.test.Client`. Example """"""" @@ -158,8 +148,9 @@ Example response = client.get('/') assert response.content == 'Foobar' -To use `client` as an authenticated standard user, call its `force_login()` or -`login()` method before accessing a URL: +To use `client` as an authenticated standard user, call its +:meth:`force_login() ` or +:meth:`login() ` method before accessing a URL: :: @@ -178,7 +169,7 @@ To use `client` as an authenticated standard user, call its `force_login()` or ``admin_client`` - ``django.test.Client`` logged in as admin ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -An instance of a `django.test.Client`_, logged in as an admin user. +An instance of a :class:`django.test.Client`, logged in as an admin user. Example """"""" @@ -208,7 +199,8 @@ Using the `admin_user` fixture will cause the test to automatically be marked fo ~~~~~~~~~~~~~~~~~~~~~ A shortcut to the User model configured for use by the current Django project (aka the model referenced by -`settings.AUTH_USER_MODEL`). Use this fixture to make pluggable apps testable regardless what User model is configured +`settings.AUTH_USER_MODEL `_). +Use this fixture to make pluggable apps testable regardless what User model is configured in the containing Django project. Example @@ -223,8 +215,9 @@ Example ``django_username_field`` ~~~~~~~~~~~~~~~~~~~~~~~~~ -This fixture extracts the field name used for the username on the user model, i.e. resolves to the current -``settings.USERNAME_FIELD``. Use this fixture to make pluggable apps testable regardless what the username field +This fixture extracts the field name used for the username on the user model, i.e. +resolves to the user model's :attr:`~django.contrib.auth.models.CustomUser.USERNAME_FIELD`. +Use this fixture to make pluggable apps testable regardless what the username field is configured to be in the containing Django project. ``db`` @@ -264,7 +257,7 @@ normally use the ``pytest.mark.django_db`` mark with ``transaction=True`` and `` This fixture runs a live Django server in a background thread. The server's URL can be retrieved using the ``live_server.url`` attribute -or by requesting it's string value: ``unicode(live_server)``. You can +or by requesting it's string value: ``str(live_server)``. You can also directly concatenate a string to form a URL: ``live_server + '/foo``. @@ -313,8 +306,8 @@ This fixture allows to check for an expected number of DB queries. If the assertion failed, the executed queries can be shown by using the verbose command line option. -It wraps `django.test.utils.CaptureQueriesContext` and yields the wrapped -CaptureQueriesContext instance. +It wraps ``django.test.utils.CaptureQueriesContext`` and yields the wrapped +``CaptureQueriesContext`` instance. Example usage:: diff --git a/docs/index.rst b/docs/index.rst index 49ecc4d59..46ebf7354 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -39,14 +39,13 @@ Why would I use this instead of Django's manage.py test command? Running the test suite with pytest offers some features that are not present in Django's standard test mechanism: * Less boilerplate: no need to import unittest, create a subclass with methods. Just write tests as regular functions. -* `Manage test dependencies with fixtures`_. +* :ref:`Manage test dependencies with fixtures `. * Run tests in multiple processes for increased speed. * There are a lot of other nice plugins available for pytest. * Easy switching: Existing unittest-style tests will still work without any modifications. See the `pytest documentation`_ for more information on pytest. -.. _Manage test dependencies with fixtures: http://docs.pytest.org/en/latest/fixture.html .. _pytest documentation: http://docs.pytest.org/ Bugs? Feature Suggestions? diff --git a/docs/tutorial.rst b/docs/tutorial.rst index ee4ec00c2..e08b0724f 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -21,7 +21,7 @@ Talks, articles and blog posts John Costa `_. -For general information and tutorials on pytest, see the `pytest tutorial page `_. +For general information and tutorials on pytest, see the `pytest tutorial page `_. Step 1: Installation diff --git a/docs/usage.rst b/docs/usage.rst index 4c357e0ea..58c7e56ba 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -20,7 +20,7 @@ the command line:: pytest test_something.py a_directory See the `pytest documentation on Usage and invocations -`_ for more help on available parameters. +`_ for more help on available parameters. Additional command line options ------------------------------- @@ -51,6 +51,6 @@ is set to "foo", the test database with xdist will be "test_foo_gw0", "test_foo_gw1" etc. See the full documentation on `pytest-xdist -`_ for more information. Among other -features, pytest-xdist can distribute/coordinate test execution on remote -machines. +`_ for more +information. Among other features, pytest-xdist can distribute/coordinate test +execution on remote machines. From 1c11e0e5e66d38d15880a29cb0db078997ce7cc1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 10 Oct 2020 19:55:19 +0300 Subject: [PATCH 030/150] docs: replace http:// -> https:// and fix some broken links --- docs/changelog.rst | 4 ++-- docs/contributing.rst | 18 +++++++++--------- docs/database.rst | 4 ++-- docs/faq.rst | 4 ++-- docs/index.rst | 4 ++-- docs/tutorial.rst | 6 +++--- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 18fb35be0..506a0cff8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -656,7 +656,7 @@ bugs. The tests for pytest-django itself has been greatly improved, paving the way for easier additions of new and exciting features in the future! -* Semantic version numbers will now be used for releases, see http://semver.org/. +* Semantic version numbers will now be used for releases, see https://semver.org/. * Do not allow database access in tests by default. Introduce ``pytest.mark.django_db`` to enable database access. @@ -720,7 +720,7 @@ v1.1 ---- * The initial release of this fork from `Ben Firshman original project - `__ + `__ * Added documentation * Uploaded to PyPI for easy installation * Added the ``transaction_test_case`` decorator for tests that needs real transactions diff --git a/docs/contributing.rst b/docs/contributing.rst index 705cbc5ed..b5f1b7b92 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -227,16 +227,16 @@ double cookie points. Seriously. You rock. .. _fork: https://github.com/pytest-dev/pytest-django .. _issue tracker: https://github.com/pytest-dev/pytest-django/issues -.. _Sphinx: http://sphinx.pocoo.org/ -.. _PEP8: http://www.python.org/dev/peps/pep-0008/ -.. _GitHub : http://www.github.com -.. _GitHub help : http://help.github.com -.. _freenode : http://freenode.net/ +.. _Sphinx: https://www.sphinx-doc.org/ +.. _PEP8: https://www.python.org/dev/peps/pep-0008/ +.. _GitHub : https://www.github.com +.. _GitHub help : https://help.github.com +.. _freenode : https://freenode.net/ .. _@andreaspelme : https://twitter.com/andreaspelme -.. _pull request : http://help.github.com/send-pull-requests/ -.. _git : http://git-scm.com/ -.. _restructuredText: http://docutils.sourceforge.net/docs/ref/rst/introduction.html +.. _pull request : https://help.github.com/send-pull-requests/ +.. _git : https://git-scm.com/ +.. _restructuredText: https://docutils.sourceforge.io/docs/ref/rst/introduction.html .. _django CMS: https://www.django-cms.org/ .. _GitHub Actions: https://github.com/features/actions .. _pytest-django Actions: https://github.com/pytest-dev/pytest-django/actions -.. _`subprocess section of coverage documentation`: http://nedbatchelder.com/code/coverage/subprocess.html +.. _`subprocess section of coverage documentation`: https://coverage.readthedocs.io/en/latest/subprocess.html diff --git a/docs/database.rst b/docs/database.rst index 0f00fc95c..dfe1ef150 100644 --- a/docs/database.rst +++ b/docs/database.rst @@ -79,8 +79,8 @@ directly in ``pytest-django`` please get in touch, we are interested in eventually supporting this but unsure about simply following Django's approach. -See `https://github.com/pytest-dev/pytest-django/pull/431` for an idea / -discussion to approach this. +See `pull request 431 `_ +for an idea/discussion to approach this. ``--reuse-db`` - reuse the testing database between test runs -------------------------------------------------------------- diff --git a/docs/faq.rst b/docs/faq.rst index fd08dd810..0249ebc78 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -54,7 +54,7 @@ When debugging test collection problems, the ``--collectonly`` flag and ``-rs`` (report skipped tests) can be helpful. .. _related pytest docs: - http://docs.pytest.org/en/stable/example/pythoncollection.html#changing-naming-conventions + https://docs.pytest.org/en/stable/example/pythoncollection.html#changing-naming-conventions Does pytest-django work with the pytest-xdist plugin? ----------------------------------------------------- @@ -144,6 +144,6 @@ pytest-django. Direct help can be found in the #pylib IRC channel on irc.freenode.org. -.. _pytest tag: http://stackoverflow.com/search?q=pytest +.. _pytest tag: https://stackoverflow.com/search?q=pytest .. _open an issue on the GitHub project: https://github.com/pytest-dev/pytest-django/issues/ diff --git a/docs/index.rst b/docs/index.rst index 46ebf7354..9b810e5ca 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -46,14 +46,14 @@ Running the test suite with pytest offers some features that are not present in See the `pytest documentation`_ for more information on pytest. -.. _pytest documentation: http://docs.pytest.org/ +.. _pytest documentation: https://docs.pytest.org/ Bugs? Feature Suggestions? ========================== Report issues and feature requests at the `GitHub issue tracker`_. -.. _GitHub issue tracker: http://github.com/pytest-dev/pytest-django/issues +.. _GitHub issue tracker: https://github.com/pytest-dev/pytest-django/issues Table of Contents ================= diff --git a/docs/tutorial.rst b/docs/tutorial.rst index e08b0724f..d68635d2e 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -13,9 +13,9 @@ Talks, articles and blog posts * Talk from DjangoCon Europe 2014: `pytest: helps you write better Django apps, by Andreas Pelme `_ - * Talk from EuroPython 2013: `Testing Django application with pytest, by Andreas Pelme `_ + * Talk from EuroPython 2013: `Testing Django application with pytest, by Andreas Pelme `_ - * Three part blog post tutorial (part 3 mentions Django integration): `pytest: no-boilerplate testing, by Daniel Greenfeld `_ + * Three part blog post tutorial (part 3 mentions Django integration): `pytest: no-boilerplate testing, by Daniel Greenfeld `_ * Blog post: `Django Projects to Django Apps: Converting the Unit Tests, by John Costa @@ -28,7 +28,7 @@ Step 1: Installation -------------------- pytest-django can be obtained directly from `PyPI -`_, and can be installed with +`_, and can be installed with ``pip``: .. code-block:: bash From 4cab057ec41d6368d1ec05881570593c1f1616a1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 10 Oct 2020 20:31:51 +0300 Subject: [PATCH 031/150] docs: fix broken markup in pytest.mark.django_db doc --- docs/helpers.rst | 58 ++++++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/docs/helpers.rst b/docs/helpers.rst index 9ee86868e..541a91a8c 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -24,35 +24,35 @@ Markers ``pytest.mark.django_db`` - request database access ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. :py:function:: pytest.mark.django_db([transaction=False, reset_sequences=False]): - -This is used to mark a test function as requiring the database. It -will ensure the database is set up correctly for the test. Each test -will run in its own transaction which will be rolled back at the end -of the test. This behavior is the same as Django's standard -:class:`~django.test.TestCase` class. - -In order for a test to have access to the database it must either -be marked using the ``django_db`` mark or request one of the ``db``, -``transactional_db`` or ``django_db_reset_sequences`` fixtures. Otherwise the -test will fail when trying to access the database. - -:type transaction: bool -:param transaction: - The ``transaction`` argument will allow the test to use real transactions. - With ``transaction=False`` (the default when not specified), transaction - operations are noops during the test. This is the same behavior that - :class:`django.test.TestCase` uses. When ``transaction=True``, the behavior - will be the same as :class:`django.test.TransactionTestCase`. - - -:type reset_sequences: bool -:param reset_sequences: - The ``reset_sequences`` argument will ask to reset auto increment sequence - values (e.g. primary keys) before running the test. Defaults to - ``False``. Must be used together with ``transaction=True`` to have an - effect. Please be aware that not all databases support this feature. - For details see :py:attr:`django.test.TransactionTestCase.reset_sequences`. +.. py:function:: pytest.mark.django_db([transaction=False, reset_sequences=False]) + + This is used to mark a test function as requiring the database. It + will ensure the database is set up correctly for the test. Each test + will run in its own transaction which will be rolled back at the end + of the test. This behavior is the same as Django's standard + :class:`~django.test.TestCase` class. + + In order for a test to have access to the database it must either + be marked using the ``django_db`` mark or request one of the ``db``, + ``transactional_db`` or ``django_db_reset_sequences`` fixtures. Otherwise the + test will fail when trying to access the database. + + :type transaction: bool + :param transaction: + The ``transaction`` argument will allow the test to use real transactions. + With ``transaction=False`` (the default when not specified), transaction + operations are noops during the test. This is the same behavior that + :class:`django.test.TestCase` uses. When ``transaction=True``, the behavior + will be the same as :class:`django.test.TransactionTestCase`. + + + :type reset_sequences: bool + :param reset_sequences: + The ``reset_sequences`` argument will ask to reset auto increment sequence + values (e.g. primary keys) before running the test. Defaults to + ``False``. Must be used together with ``transaction=True`` to have an + effect. Please be aware that not all databases support this feature. + For details see :py:attr:`django.test.TransactionTestCase.reset_sequences`. .. note:: From a079cd6e923b2c14bbd8e0e2ba343441334e107e Mon Sep 17 00:00:00 2001 From: Jannis Vajen Date: Tue, 19 May 2020 14:38:58 +0200 Subject: [PATCH 032/150] tests: Use realistic custom user model The previous test setup inherited from AbstractUser thus MyCustomUser still had a username field. The problems people are having when using the admin_client fixture in combination with a custom user model are due to the username field not being present. This change accounts for the more realistic scenario. --- tests/test_fixtures.py | 43 ++++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 26b5394aa..602d90fcd 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -483,15 +483,40 @@ def test_with_live_server(live_server): def test_custom_user_model(django_testdir, username_field): django_testdir.create_app_file( """ - from django.contrib.auth.models import AbstractUser + from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin from django.db import models - class MyCustomUser(AbstractUser): + class MyCustomUserManager(BaseUserManager): + def create_user(self, {username_field}, password=None, **extra_fields): + extra_fields.setdefault('is_staff', False) + extra_fields.setdefault('is_superuser', False) + user = self.model({username_field}={username_field}, **extra_fields) + user.set_password(password) + user.save() + return user + + def create_superuser(self, {username_field}, password=None, **extra_fields): + extra_fields.setdefault('is_staff', True) + extra_fields.setdefault('is_superuser', True) + return self.create_user( + {username_field}={username_field}, + password=password, + **extra_fields + ) + + class MyCustomUser(AbstractBaseUser, PermissionsMixin): + email = models.EmailField(max_length=100, unique=True) identifier = models.CharField(unique=True, max_length=100) + is_staff = models.BooleanField( + 'staff status', + default=False, + help_text='Designates whether the user can log into this admin site.' + ) - USERNAME_FIELD = '%s' - """ - % (username_field), + objects = MyCustomUserManager() + + USERNAME_FIELD = '{username_field}' + """.format(username_field=username_field), "models.py", ) django_testdir.create_app_file( @@ -551,19 +576,13 @@ class Migration(migrations.Migration): ('password', models.CharField(max_length=128, verbose_name='password')), ('last_login', models.DateTimeField(null=True, verbose_name='last login', blank=True)), ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, max_length=30, validators=[django.core.validators.RegexValidator(r'^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.', 'invalid')], help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True, verbose_name='username')), - ('first_name', models.CharField(max_length=30, verbose_name='first name', blank=True)), - ('last_name', models.CharField(max_length=30, verbose_name='last name', blank=True)), - ('email', models.EmailField(max_length=254, verbose_name='email address', blank=True)), + ('email', models.EmailField(error_messages={'unique': 'A user with that email address already exists.'}, max_length=100, unique=True, verbose_name='email address')), ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), ('identifier', models.CharField(unique=True, max_length=100)), ('groups', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', verbose_name='groups')), ('user_permissions', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Permission', blank=True, help_text='Specific permissions for this user.', verbose_name='user permissions')), ], options={ - 'abstract': False, 'verbose_name': 'user', 'verbose_name_plural': 'users', }, From 503ef3ec978c8f7ba6e90237ddbdba3ff28e0860 Mon Sep 17 00:00:00 2001 From: Jannis Vajen Date: Tue, 19 May 2020 14:47:23 +0200 Subject: [PATCH 033/150] Fix admin_client & admin_user for custom user model Relying on `User.USERNAME_FIELD` is enough for light customization and doesn't require `extra_arguments`. Esoteric user models probably need a specially tailored manager anyway and most likely a custom `django_user` fixture that goes with that. --- pytest_django/fixtures.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index df163d661..392ca6d86 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -293,11 +293,12 @@ def admin_user(db, django_user_model, django_username_field): # so we can assume so as well. user = UserModel._default_manager.get_by_natural_key(username) except UserModel.DoesNotExist: - extra_fields = {} - if username_field not in ("username", "email"): - extra_fields[username_field] = "admin" user = UserModel._default_manager.create_superuser( - username, "admin@example.com", "password", **extra_fields + **{ + "email": "admin@example.com", + "password": "password", + username_field: username, + } ) return user From c7f27cb9a8ad0bd7971c36a795fa5dc7c5d3e40f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 16 Oct 2020 15:36:41 +0300 Subject: [PATCH 034/150] tests: improve mysql settings a bit --- pytest_django_test/settings_mysql_innodb.py | 12 ++++++++++-- pytest_django_test/settings_mysql_myisam.py | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/pytest_django_test/settings_mysql_innodb.py b/pytest_django_test/settings_mysql_innodb.py index 5adc36526..a3163b096 100644 --- a/pytest_django_test/settings_mysql_innodb.py +++ b/pytest_django_test/settings_mysql_innodb.py @@ -2,6 +2,7 @@ from .settings_base import * # noqa: F401 F403 + DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", @@ -9,6 +10,13 @@ "USER": environ.get("TEST_DB_USER", "root"), "PASSWORD": environ.get("TEST_DB_PASSWORD", ""), "HOST": environ.get("TEST_DB_HOST", "localhost"), - "OPTIONS": {"init_command": "SET default_storage_engine=InnoDB"}, - } + "OPTIONS": { + "init_command": "SET default_storage_engine=InnoDB", + "charset": "utf8mb4", + }, + "TEST": { + "CHARSET": "utf8mb4", + "COLLATION": "utf8mb4_unicode_ci", + }, + }, } diff --git a/pytest_django_test/settings_mysql_myisam.py b/pytest_django_test/settings_mysql_myisam.py index 8e9106935..c4f9fc592 100644 --- a/pytest_django_test/settings_mysql_myisam.py +++ b/pytest_django_test/settings_mysql_myisam.py @@ -2,6 +2,7 @@ from .settings_base import * # noqa: F401 F403 + DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", @@ -9,6 +10,13 @@ "USER": environ.get("TEST_DB_USER", "root"), "PASSWORD": environ.get("TEST_DB_PASSWORD", ""), "HOST": environ.get("TEST_DB_HOST", "localhost"), - "OPTIONS": {"init_command": "SET default_storage_engine=MyISAM"}, - } + "OPTIONS": { + "init_command": "SET default_storage_engine=MyISAM", + "charset": "utf8mb4", + }, + "TEST": { + "CHARSET": "utf8mb4", + "COLLATION": "utf8mb4_unicode_ci", + }, + }, } From 267748debda3f020b332aa42aea4311a18421fab Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 16 Oct 2020 16:15:59 +0300 Subject: [PATCH 035/150] Officially support Python 3.9 --- .github/workflows/main.yml | 8 ++++---- setup.cfg | 1 + tox.ini | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cf002457c..9596daf98 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -53,8 +53,8 @@ jobs: python: 3.8 allow_failure: false - - name: py38-dj31-postgres-xdist-coverage - python: 3.8 + - name: py39-dj31-postgres-xdist-coverage + python: 3.9 allow_failure: false - name: py37-dj30-mysql_innodb-coverage @@ -77,8 +77,8 @@ jobs: python: 3.8 allow_failure: false - - name: py38-djmaster-sqlite-coverage - python: 3.8 + - name: py39-djmaster-sqlite-coverage + python: 3.9 allow_failure: true # Explicitly test (older) pytest 5.4. diff --git a/setup.cfg b/setup.cfg index fd2f3b4d9..9c22c6efa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,6 +24,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Software Development :: Testing diff --git a/tox.ini b/tox.ini index c24da8e9e..aa7ee4731 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] envlist = + py39-dj{31,30,22}-postgres py38-dj{31,30,22}-postgres py37-dj{31,30,22}-postgres py36-dj{31,30,22}-postgres From 8ebd49d47add2b62433377f383f8045e24e6969f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 16 Oct 2020 16:10:13 +0300 Subject: [PATCH 036/150] Release 4.0.0 --- docs/changelog.rst | 48 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 506a0cff8..6c8af07c8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,54 @@ Changelog ========= +v4.0.0 (2020-10-16) +------------------- + +Compatibility +^^^^^^^^^^^^^ + +This release contains no breaking changes, except dropping compatibility +with some older/unsupported versions. + +* Drop support for Python versions before 3.5 (#868). + + Previously 2.7 and 3.4 were supported. Running ``pip install pytest-django`` + on Python 2.7 or 3.4 would continue to install the compatible 3.x series. + +* Drop support for Django versions before 2.2 (#868). + + Previously Django>=1.8 was supported. + +* Drop support for pytest versions before 5.4 (#868). + + Previously pytest>=3.6 was supported. + +Improvements +^^^^^^^^^^^^ + +* Officialy support Python 3.9. + +* Add ``pytest_django.__version__`` (#880). + +* Minor documentation improvements (#882). + +Bugfixes +^^^^^^^^ + +* Make the ``admin_user`` and ``admin_client`` fixtures compatible with custom + user models which don't have a ``username`` field (#457). + +* Change the ``admin_user`` fixture to use ``get_by_natural_key()`` to get the + user instead of directly using ``USERNAME_FIELD``, in case it is overridden, + and to match Django (#879). + +Misc +^^^^ + +* Fix pytest-django's own tests failing due to some deprecation warnings + (#875). + + v3.10.0 (2020-08-25) -------------------- From 406fd9409c514d2972b13bd4559141fec87c59e3 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 16 Oct 2020 16:39:18 +0300 Subject: [PATCH 037/150] ci: trigger on tags --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9596daf98..ae71138be 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,6 +4,8 @@ on: push: branches: - master + tags: + - "*" pull_request: branches: - master From 53d5fa44e445cb18f28607036db36d898bbcc15b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 16 Oct 2020 16:42:35 +0300 Subject: [PATCH 038/150] setup.cfg: change universal to 0 now that we dropped python 2 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 9c22c6efa..a9623e44b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -58,7 +58,7 @@ DJANGO_SETTINGS_MODULE = pytest_django_test.settings_sqlite_file testpaths = tests [wheel] -universal = 1 +universal = 0 [flake8] # W503 line break before binary operator From d1fd55e68441357272d0223f7af54f83773d1c5e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 16 Oct 2020 17:12:42 +0300 Subject: [PATCH 039/150] Try to fix the readthedocs build --- .readthedocs.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .readthedocs.yml diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 000000000..27f596bb7 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,16 @@ +version: 2 + +sphinx: + configuration: docs/conf.py + +python: + version: 3 + install: + - method: pip + path: . + extra_requirements: + - docs + +formats: + - epub + - pdf From c0d2b7937f303e86b906858b157d4a37f07098c7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 16 Oct 2020 17:17:28 +0300 Subject: [PATCH 040/150] codecov: turn off annoying project/changes statuses --- codecov.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codecov.yml b/codecov.yml index 39bbacc0e..f1cc86973 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,6 +1,6 @@ +# reference: https://docs.codecov.io/docs/codecovyml-reference coverage: status: - project: true patch: true - changes: true + project: false comment: false From de7e2d747c55542cc4b6bd99a428a4fab6a89ee5 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 16 Oct 2020 22:57:36 +0300 Subject: [PATCH 041/150] docs: fix inaccurate comment on django_db mark on fixtures Fixes #541. --- docs/helpers.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/helpers.rst b/docs/helpers.rst index 541a91a8c..b96ffd9c6 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -56,12 +56,12 @@ Markers .. note:: - If you want access to the Django database *inside a fixture* - this marker will not help even if the function requesting your - fixture has this marker applied. To access the database in a - fixture, the fixture itself will have to request the ``db``, - ``transactional_db`` or ``django_db_reset_sequences`` fixture. See below - for a description of them. + If you want access to the Django database inside a *fixture*, this marker may + or may not help even if the function requesting your fixture has this marker + applied, depending on pytest's fixture execution order. To access the + database in a fixture, it is recommended that the fixture explicitly request + one of the ``db``, ``transactional_db`` or ``django_db_reset_sequences`` + fixtures. See below for a description of them. .. note:: Automatic usage with ``django.test.TestCase``. From c0d10af86eb737f5ac4ba42b1f4361b66d3c6c18 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 16 Oct 2020 23:24:07 +0300 Subject: [PATCH 042/150] Make admin_user work for custom user models without an email field Fixes #439. --- pytest_django/fixtures.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 392ca6d86..1dd4d0457 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -293,13 +293,12 @@ def admin_user(db, django_user_model, django_username_field): # so we can assume so as well. user = UserModel._default_manager.get_by_natural_key(username) except UserModel.DoesNotExist: - user = UserModel._default_manager.create_superuser( - **{ - "email": "admin@example.com", - "password": "password", - username_field: username, - } - ) + user_data = {} + if "email" in UserModel.REQUIRED_FIELDS: + user_data["email"] = "admin@example.com" + user_data["password"] = "password" + user_data[username_field] = username + user = UserModel._default_manager.create_superuser(**user_data) return user From 65a25250de7a021f0f44f58c16adcc077d0e89d7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 17 Oct 2020 00:00:55 +0300 Subject: [PATCH 043/150] Let django's setup/teardown set DEBUG for us It properly restores it as well. --- pytest_django/plugin.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index a13b2b81a..3dfd193a5 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -445,11 +445,9 @@ def django_test_environment(request): """ if django_settings_is_configured(): _setup_django() - from django.conf import settings as dj_settings from django.test.utils import setup_test_environment, teardown_test_environment - dj_settings.DEBUG = False - setup_test_environment() + setup_test_environment(debug=False) request.addfinalizer(teardown_test_environment) From fdf417400e622735c757ac2a717e2005e6c82ba6 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 17 Oct 2020 00:28:03 +0300 Subject: [PATCH 044/150] Add django_debug_mode to configure how DEBUG is set in tests Fixes #846. --- docs/usage.rst | 19 +++++++ pytest_django/plugin.py | 14 +++++- tests/test_django_settings_module.py | 74 +++++++++++++++++++++++++++- 3 files changed, 105 insertions(+), 2 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index 58c7e56ba..b15fc15cc 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -29,6 +29,25 @@ Additional command line options ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Fail tests that render templates which make use of invalid template variables. +Additional pytest.ini settings +------------------------------ + +``django_debug_mode`` - change how DEBUG is set +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default tests run with the +`DEBUG `_ +setting set to ``False``. This is to ensure that the observed output of your +code matches what will be seen in a production setting. + +If you want ``DEBUG`` to be set:: + + [pytest] + django_debug_mode = true + +You can also use ``django_debug_mode = keep`` to disable the overriding and use +whatever is already set in the Django settings. + Running tests in parallel with pytest-xdist ------------------------------------------- pytest-django supports running tests on multiple processes to speed up test diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 3dfd193a5..a7045cee5 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -118,6 +118,12 @@ def pytest_addoption(parser): type="bool", default=True, ) + parser.addini( + "django_debug_mode", + "How to set the Django DEBUG setting (default `False`). " + "Use `keep` to not override.", + default="False", + ) group.addoption( "--fail-on-template-vars", action="store_true", @@ -447,7 +453,13 @@ def django_test_environment(request): _setup_django() from django.test.utils import setup_test_environment, teardown_test_environment - setup_test_environment(debug=False) + debug_ini = request.config.getini("django_debug_mode") + if debug_ini == "keep": + debug = None + else: + debug = _get_boolean_value(debug_ini, False) + + setup_test_environment(debug=debug) request.addfinalizer(teardown_test_environment) diff --git a/tests/test_django_settings_module.py b/tests/test_django_settings_module.py index da16ff543..9040b522e 100644 --- a/tests/test_django_settings_module.py +++ b/tests/test_django_settings_module.py @@ -278,7 +278,7 @@ def test_settings(): assert result.ret == 0 -def test_debug_false(testdir, monkeypatch): +def test_debug_false_by_default(testdir, monkeypatch): monkeypatch.delenv("DJANGO_SETTINGS_MODULE") testdir.makeconftest( """ @@ -307,6 +307,78 @@ def test_debug_is_false(): assert r.ret == 0 +@pytest.mark.parametrize('django_debug_mode', (False, True)) +def test_django_debug_mode_true_false(testdir, monkeypatch, django_debug_mode): + monkeypatch.delenv("DJANGO_SETTINGS_MODULE") + testdir.makeini( + """ + [pytest] + django_debug_mode = {} + """.format(django_debug_mode) + ) + testdir.makeconftest( + """ + from django.conf import settings + + def pytest_configure(): + settings.configure(SECRET_KEY='set from pytest_configure', + DEBUG=%s, + DATABASES={'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:'}}, + INSTALLED_APPS=['django.contrib.auth', + 'django.contrib.contenttypes',]) + """ % (not django_debug_mode) + ) + + testdir.makepyfile( + """ + from django.conf import settings + def test_debug_is_false(): + assert settings.DEBUG is {} + """.format(django_debug_mode) + ) + + r = testdir.runpytest_subprocess() + assert r.ret == 0 + + +@pytest.mark.parametrize('settings_debug', (False, True)) +def test_django_debug_mode_keep(testdir, monkeypatch, settings_debug): + monkeypatch.delenv("DJANGO_SETTINGS_MODULE") + testdir.makeini( + """ + [pytest] + django_debug_mode = keep + """ + ) + testdir.makeconftest( + """ + from django.conf import settings + + def pytest_configure(): + settings.configure(SECRET_KEY='set from pytest_configure', + DEBUG=%s, + DATABASES={'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:'}}, + INSTALLED_APPS=['django.contrib.auth', + 'django.contrib.contenttypes',]) + """ % settings_debug + ) + + testdir.makepyfile( + """ + from django.conf import settings + def test_debug_is_false(): + assert settings.DEBUG is {} + """.format(settings_debug) + ) + + r = testdir.runpytest_subprocess() + assert r.ret == 0 + + @pytest.mark.django_project( extra_settings=""" INSTALLED_APPS = [ From b97cdb9402d63a547b8b0bd3fb39e16119a63517 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 17 Oct 2020 01:05:42 +0300 Subject: [PATCH 045/150] docs: remove note only relevant for unsupported Django --- docs/helpers.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/helpers.rst b/docs/helpers.rst index b96ffd9c6..d4df76f6b 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -98,8 +98,7 @@ Markers Ignore errors when using the ``--fail-on-template-vars`` option, i.e. do not cause tests to fail if your templates contain invalid variables. - This marker sets the ``string_if_invalid`` template option, or - the older ``settings.TEMPLATE_STRING_IF_INVALID=None`` (Django up to 1.10). + This marker sets the ``string_if_invalid`` template option. See :ref:`django:invalid-template-variables`. Example usage:: From 002be61958629a9d4eec8fd34b95b0d11d1cf7ea Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 17 Oct 2020 01:13:58 +0300 Subject: [PATCH 046/150] Stop setting TEMPLATE_STRING_IF_INVALID, no longer in supported Django version --- pytest_django/plugin.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index a13b2b81a..d691577fb 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -653,11 +653,7 @@ def __mod__(self, var): from django.conf import settings as dj_settings if dj_settings.TEMPLATES: - dj_settings.TEMPLATES[0]["OPTIONS"][ - "string_if_invalid" - ] = InvalidVarException() - else: - dj_settings.TEMPLATE_STRING_IF_INVALID = InvalidVarException() + dj_settings.TEMPLATES[0]["OPTIONS"]["string_if_invalid"] = InvalidVarException() @pytest.fixture(autouse=True) From 2e564f53cbbbf4beccad465310fc51cb2780ab5e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 17 Oct 2020 01:18:35 +0300 Subject: [PATCH 047/150] Yet more TEMPLATE_STRING_IF_INVALID removals --- pytest_django/plugin.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index d691577fb..4bfef4a36 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -597,7 +597,6 @@ def __init__(self): self.fail = True def __contains__(self, key): - """There is a test for '%s' in TEMPLATE_STRING_IF_INVALID.""" return key == "%s" @staticmethod @@ -635,7 +634,6 @@ def _get_origin(): return template.name def __mod__(self, var): - """Handle TEMPLATE_STRING_IF_INVALID % var.""" origin = self._get_origin() if origin: msg = "Undefined template variable '{}' in '{}'".format(var, origin) @@ -667,8 +665,6 @@ def _template_string_if_invalid_marker(request): if dj_settings.TEMPLATES: dj_settings.TEMPLATES[0]["OPTIONS"]["string_if_invalid"].fail = False - else: - dj_settings.TEMPLATE_STRING_IF_INVALID.fail = False @pytest.fixture(autouse=True, scope="function") From 93a96f60a91f3bd2d639d62f77e65fa3ee75fb0e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 17 Oct 2020 16:39:54 +0300 Subject: [PATCH 048/150] docs: some minor fixes/improvements --- docs/helpers.rst | 56 +++++++++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/docs/helpers.rst b/docs/helpers.rst index d4df76f6b..1203c3a08 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -32,10 +32,10 @@ Markers of the test. This behavior is the same as Django's standard :class:`~django.test.TestCase` class. - In order for a test to have access to the database it must either - be marked using the ``django_db`` mark or request one of the ``db``, - ``transactional_db`` or ``django_db_reset_sequences`` fixtures. Otherwise the - test will fail when trying to access the database. + In order for a test to have access to the database it must either be marked + using the :func:`~pytest.mark.django_db` mark or request one of the :fixture:`db`, + :fixture:`transactional_db` or :fixture:`django_db_reset_sequences` fixtures. + Otherwise the test will fail when trying to access the database. :type transaction: bool :param transaction: @@ -60,8 +60,9 @@ Markers or may not help even if the function requesting your fixture has this marker applied, depending on pytest's fixture execution order. To access the database in a fixture, it is recommended that the fixture explicitly request - one of the ``db``, ``transactional_db`` or ``django_db_reset_sequences`` - fixtures. See below for a description of them. + one of the :fixture:`db`, :fixture:`transactional_db` or + :fixture:`django_db_reset_sequences` fixtures. See below for a description of + them. .. note:: Automatic usage with ``django.test.TestCase``. @@ -115,6 +116,7 @@ pytest-django provides some pytest fixtures to provide dependencies for tests. More information on fixtures is available in the :ref:`pytest documentation `. +.. fixture:: rf ``rf`` - ``RequestFactory`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -133,6 +135,8 @@ Example response = my_view(request) assert response.status_code == 200 +.. fixture:: client + ``client`` - ``django.test.Client`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -164,6 +168,7 @@ To use `client` as an authenticated standard user, call its response = client.get('/private') assert response.content == 'Protected Area' +.. fixture:: admin_client ``admin_client`` - ``django.test.Client`` logged in as admin ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -179,8 +184,8 @@ Example response = admin_client.get('/admin/') assert response.status_code == 200 -Using the `admin_client` fixture will cause the test to automatically be marked for database use (no need to specify the -``django_db`` mark). +Using the `admin_client` fixture will cause the test to automatically be marked +for database use (no need to specify the :func:`~pytest.mark.django_db` mark). .. fixture:: admin_user @@ -190,9 +195,10 @@ Using the `admin_client` fixture will cause the test to automatically be marked An instance of a superuser, with username "admin" and password "password" (in case there is no "admin" user yet). -Using the `admin_user` fixture will cause the test to automatically be marked for database use (no need to specify the -``django_db`` mark). +Using the `admin_user` fixture will cause the test to automatically be marked +for database use (no need to specify the :func:`~pytest.mark.django_db` mark). +.. fixture:: django_user_model ``django_user_model`` ~~~~~~~~~~~~~~~~~~~~~ @@ -210,6 +216,7 @@ Example def test_new_user(django_user_model): django_user_model.objects.create(username="someone", password="something") +.. fixture:: django_username_field ``django_username_field`` ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -219,37 +226,43 @@ resolves to the user model's :attr:`~django.contrib.auth.models.CustomUser.USERN Use this fixture to make pluggable apps testable regardless what the username field is configured to be in the containing Django project. +.. fixture:: db + ``db`` ~~~~~~~ -.. fixture:: db - This fixture will ensure the Django database is set up. Only required for fixtures that want to use the database themselves. A -test function should normally use the ``pytest.mark.django_db`` +test function should normally use the :func:`pytest.mark.django_db` mark to signal it needs the database. This fixture does not return a database connection object. When you need a Django database connection or cursor, import it from Django using ``from django.db import connection``. +.. fixture:: transactional_db + ``transactional_db`` ~~~~~~~~~~~~~~~~~~~~ This fixture can be used to request access to the database including transaction support. This is only required for fixtures which need database access themselves. A test function should normally use the -``pytest.mark.django_db`` mark with ``transaction=True``. +func:`pytest.mark.django_db` mark with ``transaction=True`` to signal +it needs the database. + +.. fixture:: django_db_reset_sequences ``django_db_reset_sequences`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. fixture:: django_db_reset_sequences - This fixture provides the same transactional database access as -``transactional_db``, with additional support for reset of auto increment -sequences (if your database supports it). This is only required for -fixtures which need database access themselves. A test function should -normally use the ``pytest.mark.django_db`` mark with ``transaction=True`` and ``reset_sequences=True``. +:fixture:`transactional_db`, with additional support for reset of auto +increment sequences (if your database supports it). This is only required for +fixtures which need database access themselves. A test function should normally +use the :func:`pytest.mark.django_db` mark with ``transaction=True`` and +``reset_sequences=True``. + +.. fixture:: live_server ``live_server`` ~~~~~~~~~~~~~~~ @@ -272,6 +285,8 @@ also directly concatenate a string to form a URL: ``live_server + In addition, using ``live_server`` will also trigger transactional database access, if not specified. +.. fixture:: settings + ``settings`` ~~~~~~~~~~~~ @@ -341,6 +356,7 @@ Example usage:: Item.objects.create('foo') Item.objects.create('bar') +.. fixture:: mailoutbox ``mailoutbox`` ~~~~~~~~~~~~~~ From 192b63493efba2e199abdd6d76afa30277949207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Wed, 14 Jun 2017 23:29:36 +0200 Subject: [PATCH 049/150] docs: describe how to populate the database in case you use transaction and transaction-less database. [ran: adjusted] --- docs/database.rst | 56 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/docs/database.rst b/docs/database.rst index dfe1ef150..2ed03ce8d 100644 --- a/docs/database.rst +++ b/docs/database.rst @@ -373,17 +373,17 @@ Put this into ``conftest.py``:: Populate the database with initial test data """""""""""""""""""""""""""""""""""""""""""" -This example shows how you can populate the test database with test data. The -test data will be saved in the database, i.e. it will not just be part of a -transactions. This example uses Django's fixture loading mechanism, but it can -be replaced with any way of loading data into the database. +In some cases you want to populate the test database before you start the +tests. Because of different ways you may use the test database, there are +different ways to populate it. -Notice that :fixture:`django_db_setup` is in the argument list. This may look -odd at first, but it will make sure that the original pytest-django fixture -is used to create the test database. When ``call_command`` is invoked, the -test database is already prepared and configured. +Populate the test database if you don't use transactional or live_server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Put this in ``conftest.py``:: +If you are using the :func:`pytest.mark.django_db` marker or :fixture:`db` +fixture, you probably don't want to explictly handle transactions in your +tests. In this case, it is sufficient to populate your database only +once. You can put code like this in ``conftest.py``:: import pytest @@ -392,7 +392,43 @@ Put this in ``conftest.py``:: @pytest.fixture(scope='session') def django_db_setup(django_db_setup, django_db_blocker): with django_db_blocker.unblock(): - call_command('loaddata', 'your_data_fixture.json') + call_command('loaddata', 'my_fixture.json') + +This loads the Django fixture ``my_fixture.json`` once for the entire test +session. This data will be available to tests marked with the +:func:`pytest.mark.django_db` mark, or tests which use the :fixture:`db` +fixture. The test data will be saved in the database and will not be reset. +This example uses Django's fixture loading mechanism, but it can be replaced +with any way of loading data into the database. + +Notice :fixture:`django_db_setup` in the argument list. This triggers the +original pytest-django fixture to create the test database, so that when +``call_command`` is invoked, the test database is already prepared and +configured. + +Populate the test database if you use transactional or live_server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In case you use transactional tests (you use the :func:`pytest.mark.django_db` +marker with ``transaction=True``, or the :fixture:`transactional_db` fixture), +you need to repopulate your database every time a test starts, because the +database is cleared between tests. + +The :fixture:`live_server` fixture uses :fixture:`transactional_db`, so you +also need to populate the test database this way when using it. + +You can put this code into ``conftest.py``. Note that while it it is similar to +the previous one, the scope is changed from ``session`` to ``function``:: + + import pytest + + from myapp.models import Widget + + @pytest.fixture(scope='function') + def django_db_setup(django_db_setup, django_db_blocker): + with django_db_blocker.unblock(): + Widget.objects.create(...) + Use the same database for all xdist processes """"""""""""""""""""""""""""""""""""""""""""" From ded4186cc142a8279a1a47b0aa1d207b849bd1db Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 17 Oct 2020 22:00:54 +0300 Subject: [PATCH 050/150] Remove a bunch of now-unused code Missed in ce61e0446f11b4a37077800a4cbb51153c9be2f4. --- pytest_django/plugin.py | 67 ----------------------------------------- 1 file changed, 67 deletions(-) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 701e317a1..c286b43b7 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -10,7 +10,6 @@ import os import pathlib import sys -import types import pytest @@ -333,72 +332,6 @@ def pytest_configure(): _setup_django() -def _classmethod_is_defined_at_leaf(cls, method_name): - super_method = None - - for base_cls in cls.__mro__[1:]: # pragma: no branch - super_method = base_cls.__dict__.get(method_name) - if super_method is not None: - break - - assert super_method is not None, ( - "%s could not be found in base classes" % method_name - ) - - method = getattr(cls, method_name) - - try: - f = method.__func__ - except AttributeError: - pytest.fail("{}.{} should be a classmethod".format(cls, method_name)) - return f is not super_method.__func__ - - -_disabled_classmethods = {} - - -def _disable_class_methods(cls): - if cls in _disabled_classmethods: - return - - _disabled_classmethods[cls] = ( - # Get the classmethod object (not the resulting bound method), - # otherwise inheritance will be broken when restoring. - cls.__dict__.get("setUpClass"), - _classmethod_is_defined_at_leaf(cls, "setUpClass"), - cls.__dict__.get("tearDownClass"), - _classmethod_is_defined_at_leaf(cls, "tearDownClass"), - ) - - cls.setUpClass = types.MethodType(lambda cls: None, cls) - cls.tearDownClass = types.MethodType(lambda cls: None, cls) - - -def _restore_class_methods(cls): - ( - setUpClass, - restore_setUpClass, - tearDownClass, - restore_tearDownClass, - ) = _disabled_classmethods.pop(cls) - - try: - del cls.setUpClass - except AttributeError: - raise - - try: - del cls.tearDownClass - except AttributeError: - pass - - if restore_setUpClass: - cls.setUpClass = setUpClass - - if restore_tearDownClass: - cls.tearDownClass = tearDownClass - - @pytest.hookimpl(tryfirst=True) def pytest_collection_modifyitems(items): # If Django is not configured we don't need to bother From 3e8ff2a61c1a094993cac97f4f161183ea5ca77a Mon Sep 17 00:00:00 2001 From: Petr Belskiy Date: Sun, 18 Oct 2020 23:32:02 +0300 Subject: [PATCH 051/150] add async_client and async_rf fixtures (#865) --- docs/helpers.rst | 43 ++++++++++++++++++++++++++++++++++++++- pytest_django/fixtures.py | 22 ++++++++++++++++++++ pytest_django/plugin.py | 2 ++ tests/test_fixtures.py | 15 ++++++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) diff --git a/docs/helpers.rst b/docs/helpers.rst index 1203c3a08..42147c0af 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -135,6 +135,28 @@ Example response = my_view(request) assert response.status_code == 200 +.. fixture:: async_rf + +``async_rf`` - ``AsyncRequestFactory`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An instance of a `django.test.AsyncRequestFactory` + +.. _django.test.AsyncRequestFactory: https://docs.djangoproject.com/en/3.1/topics/testing/advanced/#asyncrequestfactory + +Example +""""""" + +:: + + from myapp.views import my_view + + @pytest.mark.asyncio + async def test_details(async_rf): + request = await async_rf.get('/customer/details') + response = my_view(request) + assert response.status_code == 200 + .. fixture:: client ``client`` - ``django.test.Client`` @@ -168,6 +190,25 @@ To use `client` as an authenticated standard user, call its response = client.get('/private') assert response.content == 'Protected Area' +.. fixture:: async_client + +``async_client`` - ``django.test.AsyncClient`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An instance of a `django.test.AsyncClient` + +.. _django.test.AsyncClient: https://docs.djangoproject.com/en/stable/topics/testing/tools/#the-test-client + +Example +""""""" + +:: + + @pytest.mark.asyncio + async def test_with_async_client(async_client): + response = await async_client.get('/') + assert response.content == 'Foobar' + .. fixture:: admin_client ``admin_client`` - ``django.test.Client`` logged in as admin @@ -235,7 +276,7 @@ This fixture will ensure the Django database is set up. Only required for fixtures that want to use the database themselves. A test function should normally use the :func:`pytest.mark.django_db` mark to signal it needs the database. This fixture does -not return a database connection object. When you need a Django +not return a database connection object. When you need a Django database connection or cursor, import it from Django using ``from django.db import connection``. diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 1dd4d0457..d9f891a14 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -20,8 +20,10 @@ "django_user_model", "django_username_field", "client", + "async_client", "admin_client", "rf", + "async_rf", "settings", "live_server", "_live_server_helper", @@ -261,6 +263,16 @@ def client(): return Client() +@pytest.fixture() +def async_client(): + """A Django test async client instance.""" + skip_if_no_django() + + from django.test.client import AsyncClient + + return AsyncClient() + + @pytest.fixture() def django_user_model(db): """The class of Django's user model.""" @@ -322,6 +334,16 @@ def rf(): return RequestFactory() +@pytest.fixture() +def async_rf(): + """AsyncRequestFactory instance""" + skip_if_no_django() + + from django.test.client import AsyncRequestFactory + + return AsyncRequestFactory() + + class SettingsWrapper: _to_restore = [] diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index c286b43b7..f724888e6 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -27,12 +27,14 @@ from .fixtures import _live_server_helper # noqa from .fixtures import admin_client # noqa from .fixtures import admin_user # noqa +from .fixtures import async_client # noqa from .fixtures import client # noqa from .fixtures import db # noqa from .fixtures import django_user_model # noqa from .fixtures import django_username_field # noqa from .fixtures import live_server # noqa from .fixtures import django_db_reset_sequences # noqa +from .fixtures import async_rf # noqa from .fixtures import rf # noqa from .fixtures import settings # noqa from .fixtures import transactional_db # noqa diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 602d90fcd..e837b9b47 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -18,6 +18,7 @@ from django.utils.encoding import force_str from pytest_django_test.app.models import Item +from pytest_django.lazy_django import get_django_version @contextmanager @@ -36,6 +37,13 @@ def test_client(client): assert isinstance(client, Client) +@pytest.mark.skipif(get_django_version() < (3, 1), reason="Django >= 3.1 required") +def test_async_client(async_client): + from django.test.client import AsyncClient + + assert isinstance(async_client, AsyncClient) + + @pytest.mark.django_db def test_admin_client(admin_client): assert isinstance(admin_client, Client) @@ -73,6 +81,13 @@ def test_rf(rf): assert isinstance(rf, RequestFactory) +@pytest.mark.skipif(get_django_version() < (3, 1), reason="Django >= 3.1 required") +def test_async_rf(async_rf): + from django.test.client import AsyncRequestFactory + + assert isinstance(async_rf, AsyncRequestFactory) + + @pytest.mark.django_db def test_django_assert_num_queries_db(request, django_assert_num_queries): with nonverbose_config(request.config): From 889c847f965ad4f77328fac0ec36a994ca9b2fa1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 18 Oct 2020 23:44:21 +0300 Subject: [PATCH 052/150] docs: minor fixes to previous commit --- docs/helpers.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/helpers.rst b/docs/helpers.rst index 42147c0af..5d70ee351 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -140,13 +140,15 @@ Example ``async_rf`` - ``AsyncRequestFactory`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -An instance of a `django.test.AsyncRequestFactory` +An instance of a `django.test.AsyncRequestFactory`_. -.. _django.test.AsyncRequestFactory: https://docs.djangoproject.com/en/3.1/topics/testing/advanced/#asyncrequestfactory +.. _django.test.AsyncRequestFactory: https://docs.djangoproject.com/en/stable/topics/testing/advanced/#asyncrequestfactory Example """"""" +This example uses `pytest-asyncio `_. + :: from myapp.views import my_view @@ -195,13 +197,15 @@ To use `client` as an authenticated standard user, call its ``async_client`` - ``django.test.AsyncClient`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -An instance of a `django.test.AsyncClient` +An instance of a `django.test.AsyncClient`_. -.. _django.test.AsyncClient: https://docs.djangoproject.com/en/stable/topics/testing/tools/#the-test-client +.. _django.test.AsyncClient: https://docs.djangoproject.com/en/stable/topics/testing/tools/#testing-asynchronous-code Example """"""" +This example uses `pytest-asyncio `_. + :: @pytest.mark.asyncio From 68a7047b1ae27caa0e060e2d0f1920c34e62b19f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 20 Oct 2020 15:07:26 +0300 Subject: [PATCH 053/150] ci: fix tag on pypi-publish action --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ae71138be..aa5fbb485 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -124,7 +124,7 @@ jobs: run: python setup.py sdist bdist_wheel - name: Publish package - uses: pypa/gh-action-pypi-publish@1.4.1 + uses: pypa/gh-action-pypi-publish@v1.4.1 with: user: __token__ password: ${{ secrets.pypi_token }} From e4ebc59b0037e5623706c738ef8cbf09ecd2425d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 22 Oct 2020 21:53:01 +0300 Subject: [PATCH 054/150] Release 4.1.0 Fixes #889. --- docs/changelog.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6c8af07c8..2c24023fb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,24 @@ Changelog ========= +v4.1.0 (2020-10-22) +------------------- + +Improvements +^^^^^^^^^^^^ + +* Add the :fixture:`async_client` and :fixture:`async_rf` fixtures (#864). + +* Add :ref:`django_debug_mode ` to configure how ``DEBUG`` is set in tests (#228). + +* Documentation improvements. + +Bugfixes +^^^^^^^^ + +* Make :fixture:`admin_user` work for custom user models without an ``email`` field. + + v4.0.0 (2020-10-16) ------------------- From bc9cda36a18769ba7115a6c8324908637d0c765d Mon Sep 17 00:00:00 2001 From: minusf Date: Sat, 24 Oct 2020 23:56:18 +0200 Subject: [PATCH 055/150] bit more explicit use case for `configure` + `settings` fixture (#890) --- docs/configuring_django.rst | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/configuring_django.rst b/docs/configuring_django.rst index e5aaa1bbf..ab4a4c980 100644 --- a/docs/configuring_django.rst +++ b/docs/configuring_django.rst @@ -76,7 +76,8 @@ INI File Contents:: Using ``django.conf.settings.configure()`` ------------------------------------------ -Django settings can be set up by calling ``django.conf.settings.configure()``. +In case there is no ``DJANGO_SETTINGS_MODULE``, the ``settings`` object can be +created by calling ``django.conf.settings.configure()``. This can be done from your project's ``conftest.py`` file:: @@ -85,6 +86,22 @@ This can be done from your project's ``conftest.py`` file:: def pytest_configure(): settings.configure(DATABASES=...) +Overriding individual settings +------------------------------ + +Settings can be overridden by using the :fixture:`settings` fixture:: + + @pytest.fixture(autouse=True) + def use_dummy_cache_backend(settings): + settings.CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.dummy.DummyCache", + } + } + +Here `autouse=True` is used, meaning the fixture is automatically applied to all tests, +but it can also be requested individually per-test. + Changing your app before Django gets set up ------------------------------------------- From c187ea6f2f787b080cae1bae2aea8ffc014ebda7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 26 Oct 2020 22:54:45 +0200 Subject: [PATCH 056/150] Sort items in-place in pytest_collection_modifyitems --- pytest_django/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index f724888e6..b02f26143 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -369,7 +369,7 @@ def get_order_number(test): return 2 - items[:] = sorted(items, key=get_order_number) + items.sort(key=get_order_number) @pytest.fixture(autouse=True, scope="session") From 9e1d4a177e32f78ab4011dc22fd81b8a22fd65c3 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 26 Oct 2020 23:04:22 +0200 Subject: [PATCH 057/150] Avoid needless __import__() --- pytest_django/lazy_django.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pytest_django/lazy_django.py b/pytest_django/lazy_django.py index 9fd715bf9..e369cfe35 100644 --- a/pytest_django/lazy_django.py +++ b/pytest_django/lazy_django.py @@ -30,4 +30,6 @@ def django_settings_is_configured(): def get_django_version(): - return __import__("django").VERSION + import django + + return django.VERSION From 68d1ab9461510b63077e459fe3d7004d0ee5735e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 26 Oct 2020 23:12:41 +0200 Subject: [PATCH 058/150] Inline the migrations.py file Clearer this way. --- pytest_django/fixtures.py | 7 ++++++- pytest_django/migrations.py | 13 ------------- 2 files changed, 6 insertions(+), 14 deletions(-) delete mode 100644 pytest_django/migrations.py diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index d9f891a14..814dcbdf5 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -157,7 +157,12 @@ def _disable_native_migrations(): from django.conf import settings from django.core.management.commands import migrate - from .migrations import DisableMigrations + class DisableMigrations: + def __contains__(self, item): + return True + + def __getitem__(self, item): + return None settings.MIGRATION_MODULES = DisableMigrations() diff --git a/pytest_django/migrations.py b/pytest_django/migrations.py deleted file mode 100644 index efcabf929..000000000 --- a/pytest_django/migrations.py +++ /dev/null @@ -1,13 +0,0 @@ -# code snippet copied from https://gist.github.com/NotSqrt/5f3c76cd15e40ef62d09 -from pytest_django.lazy_django import get_django_version - - -class DisableMigrations: - def __init__(self): - self._django_version = get_django_version() - - def __contains__(self, item): - return True - - def __getitem__(self, item): - return None From bd2ae62968aaf97c6efc7e02ff77ba6160865435 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 26 Oct 2020 23:23:21 +0200 Subject: [PATCH 059/150] Decorate hook impls with pytest.hookimpl This is not strictly needed but I like it for explicitness. --- pytest_django/plugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index b02f26143..057145575 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -52,6 +52,7 @@ # ############### pytest hooks ################ +@pytest.hookimpl() def pytest_addoption(parser): group = parser.getgroup("django") group.addoption( @@ -241,6 +242,7 @@ def _get_boolean_value(x, name, default=None): ) +@pytest.hookimpl() def pytest_load_initial_conftests(early_config, parser, args): # Register the marks early_config.addinivalue_line( @@ -322,6 +324,7 @@ def _get_option_with_source(option, envname): _setup_django() +@pytest.hookimpl() def pytest_report_header(): if _report_header: return ["django: " + ", ".join(_report_header)] From 4800ecaf6e3cd0e4e24368aac98ce0dda0288d17 Mon Sep 17 00:00:00 2001 From: Max Smolens Date: Tue, 8 Dec 2020 17:22:30 -0500 Subject: [PATCH 060/150] changelog: Fix typos (#894) --- docs/changelog.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 2c24023fb..0cfe964fe 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -44,7 +44,7 @@ with some older/unsupported versions. Improvements ^^^^^^^^^^^^ -* Officialy support Python 3.9. +* Officially support Python 3.9. * Add ``pytest_django.__version__`` (#880). @@ -73,9 +73,9 @@ v3.10.0 (2020-08-25) Improvements ^^^^^^^^^^^^ -* Officialy support Django 3.1 +* Officially support Django 3.1 -* Preliminary supoprt for upcoming Django 3.2 +* Preliminary support for upcoming Django 3.2 * Support for pytest-xdist 2.0 From f9e71485d472c715831c24acff6e3a9330d4627c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Tue, 15 Dec 2020 11:59:56 +0100 Subject: [PATCH 061/150] Advertise the cool FAIL_INVALID_TEMPLATE_VARS flag (#895) --- docs/usage.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/usage.rst b/docs/usage.rst index b15fc15cc..edfead5e9 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -29,6 +29,11 @@ Additional command line options ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Fail tests that render templates which make use of invalid template variables. +You can switch it on in `pytest.ini`:: + + [pytest] + FAIL_INVALID_TEMPLATE_VARS = True + Additional pytest.ini settings ------------------------------ From 5cc98c1802c84239f27f778ede8aa1805328e953 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 26 Dec 2020 15:44:44 +0200 Subject: [PATCH 062/150] Avoid using deprecated pytest --strict --- setup.cfg | 4 ++-- tests/conftest.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index a9623e44b..01c9d536c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,9 +51,9 @@ testing = django-configurations>=2.0 [tool:pytest] -# --strict: warnings become errors. +# --strict-markers: error on using unregistered marker. # -ra: show extra test summary info for everything. -addopts = --strict -ra +addopts = --strict-markers -ra DJANGO_SETTINGS_MODULE = pytest_django_test.settings_sqlite_file testpaths = tests diff --git a/tests/conftest.py b/tests/conftest.py index 8481c8dfc..7e47e74e8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -136,7 +136,7 @@ def create_app_file(code, filename): testdir.makeini( """ [pytest] - addopts = --strict + addopts = --strict-markers console_output_style=classic """ ) From 59fdb49fbc380bc09bcac65de8acab767194b553 Mon Sep 17 00:00:00 2001 From: Ishu Kumar Date: Thu, 14 Jan 2021 19:33:50 +0530 Subject: [PATCH 063/150] Update database.rst (#900) made text changes in line 43 and 44. Added commas in both the lines for better readability, and changed `for` in line 43 to `to be` to reduce redundancy. --- docs/database.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/database.rst b/docs/database.rst index 2ed03ce8d..c158a17e1 100644 --- a/docs/database.rst +++ b/docs/database.rst @@ -40,8 +40,8 @@ documentation ` for detail:: By default ``pytest-django`` will set up the Django databases the -first time a test needs them. Once setup the database is cached for -used for all subsequent tests and rolls back transactions to isolate +first time a test needs them. Once setup, the database is cached to be +used for all subsequent tests and rolls back transactions, to isolate tests from each other. This is the same way the standard Django :class:`~django.test.TestCase` uses the database. However ``pytest-django`` also caters for transaction test cases and allows From 762cfc2f2cea6eeb859c3ddba3ac06d1799d0842 Mon Sep 17 00:00:00 2001 From: Hannes Ljungberg Date: Tue, 2 Mar 2021 20:26:53 +0100 Subject: [PATCH 064/150] Run tests against Django 3.2 --- .github/workflows/main.yml | 12 ++++++++++++ setup.cfg | 1 + tox.ini | 9 +++++---- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index aa5fbb485..5f21e88ed 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -55,6 +55,10 @@ jobs: python: 3.8 allow_failure: false + - name: py39-dj32-postgres-xdist-coverage + python: 3.9 + allow_failure: false + - name: py39-dj31-postgres-xdist-coverage python: 3.9 allow_failure: false @@ -75,6 +79,10 @@ jobs: python: 3.8 allow_failure: false + - name: py38-dj32-sqlite-xdist-coverage + python: 3.8 + allow_failure: false + - name: py38-dj31-sqlite-xdist-coverage python: 3.8 allow_failure: false @@ -96,6 +104,10 @@ jobs: python: 3.6 allow_failure: false + - name: py36-dj32-mysql_myisam-coverage + python: 3.6 + allow_failure: false + # pypy3: not included with coverage reports (much slower then). - name: pypy3-dj22-postgres python: pypy3 diff --git a/setup.cfg b/setup.cfg index 01c9d536c..e910b3ee1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,7 @@ classifiers = Framework :: Django :: 2.2 Framework :: Django :: 3.0 Framework :: Django :: 3.1 + Framework :: Django :: 3.2 Intended Audience :: Developers License :: OSI Approved :: BSD License Operating System :: OS Independent diff --git a/tox.ini b/tox.ini index aa7ee4731..edae81bef 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,9 @@ [tox] envlist = - py39-dj{31,30,22}-postgres - py38-dj{31,30,22}-postgres - py37-dj{31,30,22}-postgres - py36-dj{31,30,22}-postgres + py39-dj{32,31,30,22}-postgres + py38-dj{32,31,30,22}-postgres + py37-dj{32,31,30,22}-postgres + py36-dj{32,31,30,22}-postgres py35-dj{22}-postgres checkqa @@ -11,6 +11,7 @@ envlist = extras = testing deps = djmaster: https://github.com/django/django/archive/master.tar.gz + dj32: Django>=3.2b1,<4.0 dj31: Django>=3.1,<3.2 dj30: Django>=3.0,<3.1 dj22: Django>=2.2,<2.3 From edd33115e9ed205d241a6833839dcea5186d7d03 Mon Sep 17 00:00:00 2001 From: Hannes Ljungberg Date: Tue, 2 Mar 2021 19:52:40 +0100 Subject: [PATCH 065/150] Disable atomic durability check on non-transactional tests --- pytest_django/fixtures.py | 6 ++++++ tests/test_database.py | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 814dcbdf5..59a6dba0e 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -147,6 +147,12 @@ class ResetSequenceTestCase(django_case): django_case = ResetSequenceTestCase else: from django.test import TestCase as django_case + from django.db import transaction + transaction.Atomic._ensure_durability = False + + def reset_durability(): + transaction.Atomic._ensure_durability = True + request.addfinalizer(reset_durability) test_case = django_case(methodName="__init__") test_case._pre_setup() diff --git a/tests/test_database.py b/tests/test_database.py index 4b5a0a5d5..2607e1915 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -1,7 +1,8 @@ import pytest -from django.db import connection +from django.db import connection, transaction from django.test.testcases import connections_support_transactions +from pytest_django.lazy_django import get_django_version from pytest_django_test.app.models import Item @@ -138,6 +139,12 @@ def test_fin(self, fin): # Check finalizer has db access (teardown will fail if not) pass + @pytest.mark.skipif(get_django_version() < (3, 2), reason="Django >= 3.2 required") + def test_durable_transactions(self, all_dbs): + with transaction.atomic(durable=True): + item = Item.objects.create(name="foo") + assert Item.objects.get() == item + class TestDatabaseFixturesAllOrder: @pytest.fixture From 72e2eb058da92f6639e68712c4ac008d7096bc68 Mon Sep 17 00:00:00 2001 From: Paul Angerer <48882462+etimoz@users.noreply.github.com> Date: Fri, 5 Mar 2021 19:52:19 +0100 Subject: [PATCH 066/150] Updated documentation --nomigrations to --no-migrations (#906) --- docs/database.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/database.rst b/docs/database.rst index c158a17e1..d5df51ec6 100644 --- a/docs/database.rst +++ b/docs/database.rst @@ -121,13 +121,13 @@ A good way to use ``--reuse-db`` and ``--create-db`` can be: * When you alter your database schema, run ``pytest --create-db``, to force re-creation of the test database. -``--nomigrations`` - Disable Django migrations ----------------------------------------------- +``--no-migrations`` - Disable Django migrations +----------------------------------------------- -Using ``--nomigrations`` will disable Django migrations and create the database +Using ``--no-migrations`` (alias: ``--nomigrations``) will disable Django migrations and create the database by inspecting all models. It may be faster when there are several migrations to run in the database setup. You can use ``--migrations`` to force running -migrations in case ``--nomigrations`` is used, e.g. in ``setup.cfg``. +migrations in case ``--no-migrations`` is used, e.g. in ``setup.cfg``. .. _advanced-database-configuration: @@ -235,7 +235,7 @@ Returns whether or not to use migrations to create the test databases. The default implementation returns the value of the -``--migrations``/``--nomigrations`` command line options. +``--migrations``/``--no-migrations`` command line options. This fixture is by default requested from :fixture:`django_db_setup`. From 9db6ec8d8014a095834ebc8659e1fef3b8e99937 Mon Sep 17 00:00:00 2001 From: Fabian Gebhart Date: Sat, 13 Mar 2021 09:48:13 +0100 Subject: [PATCH 067/150] add some special characters to fix rendering of docs --- docs/helpers.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/helpers.rst b/docs/helpers.rst index 5d70ee351..be71d251b 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -292,7 +292,7 @@ database connection or cursor, import it from Django using This fixture can be used to request access to the database including transaction support. This is only required for fixtures which need database access themselves. A test function should normally use the -func:`pytest.mark.django_db` mark with ``transaction=True`` to signal +:func:`pytest.mark.django_db` mark with ``transaction=True`` to signal it needs the database. .. fixture:: django_db_reset_sequences @@ -316,7 +316,7 @@ This fixture runs a live Django server in a background thread. The server's URL can be retrieved using the ``live_server.url`` attribute or by requesting it's string value: ``str(live_server)``. You can also directly concatenate a string to form a URL: ``live_server + -'/foo``. +'/foo'``. .. note:: Combining database access fixtures. From 04134ae67852313599ba576d8452b268d881d1a6 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 13 Mar 2021 12:09:19 +0200 Subject: [PATCH 068/150] ci,tox,README: adjust to django branch master -> main --- .github/workflows/main.yml | 2 +- README.rst | 2 +- pytest_django/plugin.py | 2 +- tox.ini | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5f21e88ed..1c670c797 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -87,7 +87,7 @@ jobs: python: 3.8 allow_failure: false - - name: py39-djmaster-sqlite-coverage + - name: py39-djmain-sqlite-coverage python: 3.9 allow_failure: true diff --git a/README.rst b/README.rst index 6d1f1ba3f..1a5305577 100644 --- a/README.rst +++ b/README.rst @@ -28,7 +28,7 @@ pytest-django allows you to test your Django project/applications with the `_ * Version compatibility: - * Django: 2.2, 3.0, 3.1 and latest master branch (compatible at the time of + * Django: 2.2, 3.0, 3.1 and latest main branch (compatible at the time of each release) * Python: CPython>=3.5 or PyPy 3 * pytest: >=5.4 diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 057145575..290b8b49d 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -516,7 +516,7 @@ def _django_set_urlconf(request): def restore(): django.conf.settings.ROOT_URLCONF = original_urlconf # Copy the pattern from - # https://github.com/django/django/blob/master/django/test/signals.py#L152 + # https://github.com/django/django/blob/main/django/test/signals.py#L152 clear_url_caches() set_urlconf(None) diff --git a/tox.ini b/tox.ini index edae81bef..5ded9ede9 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ envlist = [testenv] extras = testing deps = - djmaster: https://github.com/django/django/archive/master.tar.gz + djmain: https://github.com/django/django/archive/main.tar.gz dj32: Django>=3.2b1,<4.0 dj31: Django>=3.1,<3.2 dj30: Django>=3.0,<3.1 From 11db342edf8c058d9c3c02b0a15e0c236f15c0c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Thu, 18 Mar 2021 08:43:24 +0100 Subject: [PATCH 069/150] Add `request.user` to example (#905) Newcomers get very confused if `request.user` is not set. I think it makes sense to underline this in the example. --- docs/helpers.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/helpers.rst b/docs/helpers.rst index be71d251b..d035f7a61 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -130,8 +130,13 @@ Example from myapp.views import my_view - def test_details(rf): + def test_details(rf, admin): request = rf.get('/customer/details') + # Remember that when using RequestFactory, the request does not pass + # through middleware. If your view expects fields such as request.user + # to be set, you need to set them explicitly. + # The following line sets request.user to an admin user. + request.user = admin response = my_view(request) assert response.status_code == 200 From a1795472124bb8d7e6a87a5e6492b623a9ce7d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Sat, 20 Mar 2021 22:17:09 +0100 Subject: [PATCH 070/150] Less opinionated wording (#917) I shorted the text to be less opinionated. I disagreed to the previous text: the ORM is a tool like any other tool. If it helps you, then use it. The only reason not to use it, is that it might slow your tests down. --- docs/database.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/database.rst b/docs/database.rst index d5df51ec6..d23934a3d 100644 --- a/docs/database.rst +++ b/docs/database.rst @@ -5,9 +5,7 @@ Database creation/re-use access. By default your tests will fail if they try to access the database. Only if you explicitly request database access will this be allowed. This encourages you to keep database-needing tests to a -minimum which is a best practice since next-to-no business logic -should be requiring the database. Moreover it makes it very clear -what code uses the database and catches any mistakes. +minimum which makes it very clear what code uses the database. Enabling database access in tests --------------------------------- From 3b42c4abb77d2c2690852924fad8ca5ca0a5d434 Mon Sep 17 00:00:00 2001 From: Stephen Smith Date: Tue, 16 Mar 2021 17:07:48 -0400 Subject: [PATCH 071/150] docs(readme): update readme broken-links. - [x] remove broken link line 58 - Manage test dependencies with pytest fixtures. ~~\~\~_ - Manage test dependencies with pytest fixtures. _ - [x] remove broken line line 62 - Make use of other pytest plugins . - [x] run test | Test | Result | | ----------- | :-----------: | | tests/test_asserts.py |[0%] | | tests/test_database.py | [5%] | | tests/test_environment.py | [10%] | | tests/test_fixtures.py | [17%] | | tests/test_unittest.py | [21%] | | tests/test_database.py | [24%] | | tests/test_fixtures.py | [26%] | | tests/test_asserts.py | [26%] | | tests/test_database.py |[ 39%] | | tests/test_db_access_in_repr.py | [40%] | | tests/test_db_setup.py | [47%] | | tests/test_django_configurations.py | [49%] | | tests/test_django_settings_module.py | [59%] | | tests/test_doctest.txt | [59%] | | tests/test_environment.py | [65%] | | tests/test_fixtures.py | [83%] | | tests/test_initialization.py | [83%] | | tests/test_manage_py_scan.py | [88%] | | tests/test_unittest.py | [94%] | | tests/test_urls.py | [96%] | | tests/test_without_django_loaded.py | [100%] | Fixes pytest-dev/pytest-django #914 --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 1a5305577..3360ae40c 100644 --- a/README.rst +++ b/README.rst @@ -55,11 +55,11 @@ Why would I use this instead of Django's `manage.py test` command? Running your test suite with pytest-django allows you to tap into the features that are already present in pytest. Here are some advantages: -* `Manage test dependencies with pytest fixtures. `_ +* `Manage test dependencies with pytest fixtures. `_ * Less boilerplate tests: no need to import unittest, create a subclass with methods. Write tests as regular functions. * Database re-use: no need to re-create the test database for every test run. * Run tests in multiple processes for increased speed (with the pytest-xdist plugin). -* Make use of other `pytest plugins `_. +* Make use of other `pytest plugins `_. * Works with both worlds: Existing unittest-style TestCase's still work without any modifications. See the `pytest documentation `_ for more information on pytest itself. From 47ebfaf75f309fd8da982685ed5445e426a9d71f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 10 Apr 2021 20:34:25 +0300 Subject: [PATCH 072/150] README: mention 3.2 as supported --- README.rst | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 3360ae40c..94774fba6 100644 --- a/README.rst +++ b/README.rst @@ -28,7 +28,7 @@ pytest-django allows you to test your Django project/applications with the `_ * Version compatibility: - * Django: 2.2, 3.0, 3.1 and latest main branch (compatible at the time of + * Django: 2.2, 3.0, 3.1, 3.2 and latest main branch (compatible at the time of each release) * Python: CPython>=3.5 or PyPy 3 * pytest: >=5.4 diff --git a/tox.ini b/tox.ini index 5ded9ede9..c367bfa20 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ envlist = extras = testing deps = djmain: https://github.com/django/django/archive/main.tar.gz - dj32: Django>=3.2b1,<4.0 + dj32: Django>=3.2,<4.0 dj31: Django>=3.1,<3.2 dj30: Django>=3.0,<3.1 dj22: Django>=2.2,<2.3 From 45087ecb9d0555af21a5d9feaa9a574c4eb15c77 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 10 Apr 2021 20:38:45 +0300 Subject: [PATCH 073/150] Release 4.2.0 --- docs/changelog.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0cfe964fe..9d59a7e9e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,22 @@ Changelog ========= +v4.2.0 (2021-04-10) +------------------- + +Improvements +^^^^^^^^^^^^ + +* Official Django 3.2 support. + +* Documentation improvements. + +Bugfixes +^^^^^^^^ + +* Disable atomic durability check on non-transactional tests (#910). + + v4.1.0 (2020-10-22) ------------------- From 9403c9447e875fe2d3444d4d28bc2934bc5ef508 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 7 May 2021 15:41:17 +0300 Subject: [PATCH 074/150] tests: remove pathlib2 support We depend on Python>=3.5 so it will never get used. --- tests/conftest.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7e47e74e8..7a8ec937c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,14 +1,11 @@ import copy import shutil from textwrap import dedent +import pathlib import pytest from django.conf import settings -try: - import pathlib -except ImportError: - import pathlib2 as pathlib pytest_plugins = "pytester" From 15705cd563c5707ac76258f5dd54e4808cffe578 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 7 May 2021 21:50:49 +0300 Subject: [PATCH 075/150] Fix django_debug_mode unsupported-value warning The function call missed an argument but the end result was the same, except for minor error message issue. --- pytest_django/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 290b8b49d..4894a0f0f 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -395,7 +395,7 @@ def django_test_environment(request): if debug_ini == "keep": debug = None else: - debug = _get_boolean_value(debug_ini, False) + debug = _get_boolean_value(debug_ini, "django_debug_mode", False) setup_test_environment(debug=debug) request.addfinalizer(teardown_test_environment) From 83c0a5a3ced566246b25f9150d916ea44becc74a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 7 May 2021 15:32:24 +0300 Subject: [PATCH 076/150] Add mypy linting --- pytest_django/asserts.py | 4 +++- pytest_django/fixtures.py | 5 ++--- .../app/migrations/0001_initial.py | 4 ++-- setup.cfg | 17 +++++++++++++++++ tests/test_fixtures.py | 5 ++--- tox.ini | 2 ++ 6 files changed, 28 insertions(+), 9 deletions(-) diff --git a/pytest_django/asserts.py b/pytest_django/asserts.py index 12a3fc565..1361429b4 100644 --- a/pytest_django/asserts.py +++ b/pytest_django/asserts.py @@ -1,7 +1,9 @@ """ Dynamically load all Django assertion cases and expose them for importing. """ +from typing import Set from functools import wraps + from django.test import ( TestCase, SimpleTestCase, LiveServerTestCase, TransactionTestCase @@ -21,7 +23,7 @@ def assertion_func(*args, **kwargs): __all__ = [] -assertions_names = set() +assertions_names = set() # type: Set[str] assertions_names.update( {attr for attr in vars(TestCase) if attr.startswith('assert')}, {attr for attr in vars(SimpleTestCase) if attr.startswith('assert')}, diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 59a6dba0e..e8c41ec41 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -1,6 +1,5 @@ """All pytest-django fixtures""" - - +from typing import List, Any import os from contextlib import contextmanager from functools import partial @@ -356,7 +355,7 @@ def async_rf(): class SettingsWrapper: - _to_restore = [] + _to_restore = [] # type: List[Any] def __delattr__(self, attr): from django.test import override_settings diff --git a/pytest_django_test/app/migrations/0001_initial.py b/pytest_django_test/app/migrations/0001_initial.py index 3a853e557..5b0415afc 100644 --- a/pytest_django_test/app/migrations/0001_initial.py +++ b/pytest_django_test/app/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 1.9a1 on 2016-06-22 04:33 +from typing import List, Tuple from django.db import migrations, models @@ -7,7 +7,7 @@ class Migration(migrations.Migration): initial = True - dependencies = [] + dependencies = [] # type: List[Tuple[str, str]] operations = [ migrations.CreateModel( diff --git a/setup.cfg b/setup.cfg index e910b3ee1..24d1cbf62 100644 --- a/setup.cfg +++ b/setup.cfg @@ -69,3 +69,20 @@ exclude = lib/,src/,docs/,bin/ [isort] forced_separate = tests,pytest_django,pytest_django_test + +[mypy] +disallow_any_generics = True +no_implicit_optional = True +show_error_codes = True +strict_equality = True +warn_redundant_casts = True +warn_unreachable = True +warn_unused_configs = True +no_implicit_reexport = True + +[mypy-django.*] +ignore_missing_imports = True +[mypy-configurations.*] +ignore_missing_imports = True +[mypy-psycopg2cffi.*] +ignore_missing_imports = True diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index e837b9b47..e20958761 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -3,11 +3,10 @@ Not quite all fixtures are tested here, the db and transactional_db fixtures are tested in test_database. """ - - import socket from contextlib import contextmanager -from urllib.request import urlopen, HTTPError +from urllib.error import HTTPError +from urllib.request import urlopen import pytest from django.conf import settings as real_settings diff --git a/tox.ini b/tox.ini index c367bfa20..834115290 100644 --- a/tox.ini +++ b/tox.ini @@ -53,9 +53,11 @@ commands = extras = deps = flake8 + mypy==0.812 commands = flake8 --version flake8 --statistics {posargs:pytest_django pytest_django_test tests} + mypy {posargs:pytest_django pytest_django_test tests} [testenv:doc8] extras = From 7e7af23c11c2f952b6d69dd47a5e78faed2fe70d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 7 May 2021 16:27:33 +0300 Subject: [PATCH 077/150] Add some basic type annotations --- pytest_django/asserts.py | 12 +- pytest_django/django_compat.py | 2 +- pytest_django/fixtures.py | 125 +++++++++++++-------- pytest_django/lazy_django.py | 11 +- pytest_django/live_server_helper.py | 19 ++-- pytest_django/plugin.py | 160 ++++++++++++++++----------- pytest_django_test/app/models.py | 2 +- pytest_django_test/app/views.py | 6 +- setup.cfg | 1 + tests/conftest.py | 13 ++- tests/test_asserts.py | 9 +- tests/test_database.py | 81 +++++++------- tests/test_db_access_in_repr.py | 2 +- tests/test_db_setup.py | 26 ++--- tests/test_django_configurations.py | 8 +- tests/test_django_settings_module.py | 34 +++--- tests/test_environment.py | 36 +++--- tests/test_fixtures.py | 116 ++++++++++--------- tests/test_initialization.py | 2 +- tests/test_manage_py_scan.py | 16 +-- tests/test_unittest.py | 46 ++++---- tests/test_urls.py | 8 +- tests/test_without_django_loaded.py | 16 +-- 23 files changed, 426 insertions(+), 325 deletions(-) diff --git a/pytest_django/asserts.py b/pytest_django/asserts.py index 1361429b4..2da2f5fbd 100644 --- a/pytest_django/asserts.py +++ b/pytest_django/asserts.py @@ -1,7 +1,7 @@ """ Dynamically load all Django assertion cases and expose them for importing. """ -from typing import Set +from typing import Any, Callable, Set from functools import wraps from django.test import ( @@ -9,10 +9,13 @@ LiveServerTestCase, TransactionTestCase ) +TYPE_CHECKING = False + + test_case = TestCase('run') -def _wrapper(name): +def _wrapper(name: str): func = getattr(test_case, name) @wraps(func) @@ -34,3 +37,8 @@ def assertion_func(*args, **kwargs): for assert_func in assertions_names: globals()[assert_func] = _wrapper(assert_func) __all__.append(assert_func) + + +if TYPE_CHECKING: + def __getattr__(name: str) -> Callable[..., Any]: + ... diff --git a/pytest_django/django_compat.py b/pytest_django/django_compat.py index 18a2413e5..615e47011 100644 --- a/pytest_django/django_compat.py +++ b/pytest_django/django_compat.py @@ -2,7 +2,7 @@ # this is the case before you call them. -def is_django_unittest(request_or_item): +def is_django_unittest(request_or_item) -> bool: """Returns True if the request_or_item is a Django test case, otherwise False""" from django.test import SimpleTestCase diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index e8c41ec41..878c76bde 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -1,5 +1,5 @@ """All pytest-django fixtures""" -from typing import List, Any +from typing import Any, Generator, List import os from contextlib import contextmanager from functools import partial @@ -10,6 +10,11 @@ from .django_compat import is_django_unittest from .lazy_django import skip_if_no_django +TYPE_CHECKING = False +if TYPE_CHECKING: + import django + + __all__ = [ "django_db_setup", "db", @@ -32,7 +37,7 @@ @pytest.fixture(scope="session") -def django_db_modify_db_settings_tox_suffix(): +def django_db_modify_db_settings_tox_suffix() -> None: skip_if_no_django() tox_environment = os.getenv("TOX_PARALLEL_ENV") @@ -42,7 +47,7 @@ def django_db_modify_db_settings_tox_suffix(): @pytest.fixture(scope="session") -def django_db_modify_db_settings_xdist_suffix(request): +def django_db_modify_db_settings_xdist_suffix(request) -> None: skip_if_no_django() xdist_suffix = getattr(request.config, "workerinput", {}).get("workerid") @@ -53,42 +58,44 @@ def django_db_modify_db_settings_xdist_suffix(request): @pytest.fixture(scope="session") def django_db_modify_db_settings_parallel_suffix( - django_db_modify_db_settings_tox_suffix, - django_db_modify_db_settings_xdist_suffix, -): + django_db_modify_db_settings_tox_suffix: None, + django_db_modify_db_settings_xdist_suffix: None, +) -> None: skip_if_no_django() @pytest.fixture(scope="session") -def django_db_modify_db_settings(django_db_modify_db_settings_parallel_suffix): +def django_db_modify_db_settings( + django_db_modify_db_settings_parallel_suffix: None, +) -> None: skip_if_no_django() @pytest.fixture(scope="session") -def django_db_use_migrations(request): +def django_db_use_migrations(request) -> bool: return not request.config.getvalue("nomigrations") @pytest.fixture(scope="session") -def django_db_keepdb(request): +def django_db_keepdb(request) -> bool: return request.config.getvalue("reuse_db") @pytest.fixture(scope="session") -def django_db_createdb(request): +def django_db_createdb(request) -> bool: return request.config.getvalue("create_db") @pytest.fixture(scope="session") def django_db_setup( request, - django_test_environment, + django_test_environment: None, django_db_blocker, - django_db_use_migrations, - django_db_keepdb, - django_db_createdb, - django_db_modify_db_settings, -): + django_db_use_migrations: bool, + django_db_keepdb: bool, + django_db_createdb: bool, + django_db_modify_db_settings: None, +) -> None: """Top level fixture to ensure test databases are available""" from django.test.utils import setup_databases, teardown_databases @@ -107,7 +114,7 @@ def django_db_setup( **setup_databases_args ) - def teardown_database(): + def teardown_database() -> None: with django_db_blocker.unblock(): try: teardown_databases(db_cfg, verbosity=request.config.option.verbose) @@ -123,8 +130,11 @@ def teardown_database(): def _django_db_fixture_helper( - request, django_db_blocker, transactional=False, reset_sequences=False -): + request, + django_db_blocker, + transactional: bool = False, + reset_sequences: bool = False, +) -> None: if is_django_unittest(request): return @@ -149,7 +159,7 @@ class ResetSequenceTestCase(django_case): from django.db import transaction transaction.Atomic._ensure_durability = False - def reset_durability(): + def reset_durability() -> None: transaction.Atomic._ensure_durability = True request.addfinalizer(reset_durability) @@ -158,15 +168,15 @@ def reset_durability(): request.addfinalizer(test_case._post_teardown) -def _disable_native_migrations(): +def _disable_native_migrations() -> None: from django.conf import settings from django.core.management.commands import migrate class DisableMigrations: - def __contains__(self, item): + def __contains__(self, item: str) -> bool: return True - def __getitem__(self, item): + def __getitem__(self, item: str) -> None: return None settings.MIGRATION_MODULES = DisableMigrations() @@ -179,7 +189,7 @@ def handle(self, *args, **kwargs): migrate.Command = MigrateSilentCommand -def _set_suffix_to_test_databases(suffix): +def _set_suffix_to_test_databases(suffix: str) -> None: from django.conf import settings for db_settings in settings.DATABASES.values(): @@ -201,7 +211,11 @@ def _set_suffix_to_test_databases(suffix): @pytest.fixture(scope="function") -def db(request, django_db_setup, django_db_blocker): +def db( + request, + django_db_setup: None, + django_db_blocker, +) -> None: """Require a django test database. This database will be setup with the default fixtures and will have @@ -227,7 +241,11 @@ def db(request, django_db_setup, django_db_blocker): @pytest.fixture(scope="function") -def transactional_db(request, django_db_setup, django_db_blocker): +def transactional_db( + request, + django_db_setup: None, + django_db_blocker, +) -> None: """Require a django test database with transaction support. This will re-initialise the django database for each test and is @@ -246,7 +264,11 @@ def transactional_db(request, django_db_setup, django_db_blocker): @pytest.fixture(scope="function") -def django_db_reset_sequences(request, django_db_setup, django_db_blocker): +def django_db_reset_sequences( + request, + django_db_setup: None, + django_db_blocker, +) -> None: """Require a transactional test database with sequence reset support. This behaves like the ``transactional_db`` fixture, with the addition @@ -264,7 +286,7 @@ def django_db_reset_sequences(request, django_db_setup, django_db_blocker): @pytest.fixture() -def client(): +def client() -> "django.test.client.Client": """A Django test client instance.""" skip_if_no_django() @@ -274,7 +296,7 @@ def client(): @pytest.fixture() -def async_client(): +def async_client() -> "django.test.client.AsyncClient": """A Django test async client instance.""" skip_if_no_django() @@ -284,7 +306,7 @@ def async_client(): @pytest.fixture() -def django_user_model(db): +def django_user_model(db: None): """The class of Django's user model.""" from django.contrib.auth import get_user_model @@ -292,13 +314,17 @@ def django_user_model(db): @pytest.fixture() -def django_username_field(django_user_model): +def django_username_field(django_user_model) -> str: """The fieldname for the username used with Django's user model.""" return django_user_model.USERNAME_FIELD @pytest.fixture() -def admin_user(db, django_user_model, django_username_field): +def admin_user( + db: None, + django_user_model, + django_username_field: str, +): """A Django admin user. This uses an existing user with username "admin", or creates a new one with @@ -325,7 +351,10 @@ def admin_user(db, django_user_model, django_username_field): @pytest.fixture() -def admin_client(db, admin_user): +def admin_client( + db: None, + admin_user, +) -> "django.test.client.Client": """A Django test client logged in as an admin user.""" from django.test.client import Client @@ -335,7 +364,7 @@ def admin_client(db, admin_user): @pytest.fixture() -def rf(): +def rf() -> "django.test.client.RequestFactory": """RequestFactory instance""" skip_if_no_django() @@ -345,7 +374,7 @@ def rf(): @pytest.fixture() -def async_rf(): +def async_rf() -> "django.test.client.AsyncRequestFactory": """AsyncRequestFactory instance""" skip_if_no_django() @@ -357,7 +386,7 @@ def async_rf(): class SettingsWrapper: _to_restore = [] # type: List[Any] - def __delattr__(self, attr): + def __delattr__(self, attr: str) -> None: from django.test import override_settings override = override_settings() @@ -368,19 +397,19 @@ def __delattr__(self, attr): self._to_restore.append(override) - def __setattr__(self, attr, value): + def __setattr__(self, attr: str, value) -> None: from django.test import override_settings override = override_settings(**{attr: value}) override.enable() self._to_restore.append(override) - def __getattr__(self, item): + def __getattr__(self, attr: str): from django.conf import settings - return getattr(settings, item) + return getattr(settings, attr) - def finalize(self): + def finalize(self) -> None: for override in reversed(self._to_restore): override.disable() @@ -429,7 +458,7 @@ def live_server(request): @pytest.fixture(autouse=True, scope="function") -def _live_server_helper(request): +def _live_server_helper(request) -> None: """Helper to make live_server work, internal to pytest-django. This helper will dynamically request the transactional_db fixture @@ -455,14 +484,22 @@ def _live_server_helper(request): @contextmanager -def _assert_num_queries(config, num, exact=True, connection=None, info=None): +def _assert_num_queries( + config, + num: int, + exact: bool = True, + connection=None, + info=None, +) -> Generator["django.test.utils.CaptureQueriesContext", None, None]: from django.test.utils import CaptureQueriesContext if connection is None: - from django.db import connection + from django.db import connection as conn + else: + conn = connection verbose = config.getoption("verbose") > 0 - with CaptureQueriesContext(connection) as context: + with CaptureQueriesContext(conn) as context: yield context num_performed = len(context) if exact: diff --git a/pytest_django/lazy_django.py b/pytest_django/lazy_django.py index e369cfe35..c71356534 100644 --- a/pytest_django/lazy_django.py +++ b/pytest_django/lazy_django.py @@ -1,20 +1,20 @@ """ Helpers to load Django lazily when Django settings can't be configured. """ - +from typing import Any, Tuple import os import sys import pytest -def skip_if_no_django(): +def skip_if_no_django() -> None: """Raises a skip exception when no Django settings are available""" if not django_settings_is_configured(): pytest.skip("no Django settings") -def django_settings_is_configured(): +def django_settings_is_configured() -> bool: """Return whether the Django settings module has been configured. This uses either the DJANGO_SETTINGS_MODULE environment variable, or the @@ -24,12 +24,13 @@ def django_settings_is_configured(): ret = bool(os.environ.get("DJANGO_SETTINGS_MODULE")) if not ret and "django.conf" in sys.modules: - return sys.modules["django.conf"].settings.configured + django_conf = sys.modules["django.conf"] # type: Any + return django_conf.settings.configured return ret -def get_django_version(): +def get_django_version() -> Tuple[int, int, int, str, int]: import django return django.VERSION diff --git a/pytest_django/live_server_helper.py b/pytest_django/live_server_helper.py index f61034900..de5f3635a 100644 --- a/pytest_django/live_server_helper.py +++ b/pytest_django/live_server_helper.py @@ -1,3 +1,6 @@ +from typing import Dict, Any + + class LiveServer: """The liveserver fixture @@ -5,11 +8,13 @@ class LiveServer: The ``live_server`` fixture handles creation and stopping. """ - def __init__(self, addr): + def __init__(self, addr: str) -> None: from django.db import connections from django.test.testcases import LiveServerThread from django.test.utils import modify_settings + liveserver_kwargs = {} # type: Dict[str, Any] + connections_override = {} for conn in connections.all(): # If using in-memory sqlite databases, pass the connections to @@ -22,7 +27,7 @@ def __init__(self, addr): conn.allow_thread_sharing = True connections_override[conn.alias] = conn - liveserver_kwargs = {"connections_override": connections_override} + liveserver_kwargs["connections_override"] = connections_override from django.conf import settings if "django.contrib.staticfiles" in settings.INSTALLED_APPS: @@ -53,20 +58,20 @@ def __init__(self, addr): if self.thread.error: raise self.thread.error - def stop(self): + def stop(self) -> None: """Stop the server""" self.thread.terminate() self.thread.join() @property - def url(self): + def url(self) -> str: return "http://{}:{}".format(self.thread.host, self.thread.port) - def __str__(self): + def __str__(self) -> str: return self.url - def __add__(self, other): + def __add__(self, other) -> str: return "{}{}".format(self, other) - def __repr__(self): + def __repr__(self) -> str: return "" % self.url diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 4894a0f0f..e7e2211f9 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -4,6 +4,7 @@ test database and provides some useful text fixtures. """ +from typing import Generator, List, Optional, Tuple, Union import contextlib import inspect from functools import reduce @@ -41,6 +42,12 @@ from .lazy_django import django_settings_is_configured, skip_if_no_django +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import ContextManager, NoReturn + + import django + SETTINGS_MODULE_ENV = "DJANGO_SETTINGS_MODULE" CONFIGURATION_ENV = "DJANGO_CONFIGURATION" @@ -53,7 +60,7 @@ @pytest.hookimpl() -def pytest_addoption(parser): +def pytest_addoption(parser) -> None: group = parser.getgroup("django") group.addoption( "--reuse-db", @@ -163,7 +170,7 @@ def pytest_addoption(parser): @contextlib.contextmanager -def _handle_import_error(extra_message): +def _handle_import_error(extra_message: str) -> Generator[None, None, None]: try: yield except ImportError as e: @@ -172,29 +179,29 @@ def _handle_import_error(extra_message): raise ImportError(msg) -def _add_django_project_to_path(args): - def is_django_project(path): +def _add_django_project_to_path(args) -> str: + def is_django_project(path: pathlib.Path) -> bool: try: return path.is_dir() and (path / "manage.py").exists() except OSError: return False - def arg_to_path(arg): + def arg_to_path(arg: str) -> pathlib.Path: # Test classes or functions can be appended to paths separated by :: arg = arg.split("::", 1)[0] return pathlib.Path(arg) - def find_django_path(args): - args = map(str, args) - args = [arg_to_path(x) for x in args if not x.startswith("-")] + def find_django_path(args) -> Optional[pathlib.Path]: + str_args = (str(arg) for arg in args) + path_args = [arg_to_path(x) for x in str_args if not x.startswith("-")] cwd = pathlib.Path.cwd() - if not args: - args.append(cwd) - elif cwd not in args: - args.append(cwd) + if not path_args: + path_args.append(cwd) + elif cwd not in path_args: + path_args.append(cwd) - for arg in args: + for arg in path_args: if is_django_project(arg): return arg for parent in arg.parents: @@ -209,7 +216,7 @@ def find_django_path(args): return PROJECT_NOT_FOUND -def _setup_django(): +def _setup_django() -> None: if "django" not in sys.modules: return @@ -227,10 +234,14 @@ def _setup_django(): _blocking_manager.block() -def _get_boolean_value(x, name, default=None): +def _get_boolean_value( + x: Union[None, bool, str], + name: str, + default: Optional[bool] = None, +) -> bool: if x is None: - return default - if x in (True, False): + return bool(default) + if isinstance(x, bool): return x possible_values = {"true": True, "false": False, "1": True, "0": False} try: @@ -243,7 +254,11 @@ def _get_boolean_value(x, name, default=None): @pytest.hookimpl() -def pytest_load_initial_conftests(early_config, parser, args): +def pytest_load_initial_conftests( + early_config, + parser, + args: List[str], +) -> None: # Register the marks early_config.addinivalue_line( "markers", @@ -288,7 +303,10 @@ def pytest_load_initial_conftests(early_config, parser, args): ): os.environ[INVALID_TEMPLATE_VARS_ENV] = "true" - def _get_option_with_source(option, envname): + def _get_option_with_source( + option: Optional[str], + envname: str, + ) -> Union[Tuple[str, str], Tuple[None, None]]: if option: return option, "option" if envname in os.environ: @@ -325,41 +343,43 @@ def _get_option_with_source(option, envname): @pytest.hookimpl() -def pytest_report_header(): +def pytest_report_header() -> Optional[List[str]]: if _report_header: return ["django: " + ", ".join(_report_header)] + return None @pytest.hookimpl(trylast=True) -def pytest_configure(): +def pytest_configure() -> None: # Allow Django settings to be configured in a user pytest_configure call, # but make sure we call django.setup() _setup_django() @pytest.hookimpl(tryfirst=True) -def pytest_collection_modifyitems(items): +def pytest_collection_modifyitems(items: List[pytest.Item]) -> None: # If Django is not configured we don't need to bother if not django_settings_is_configured(): return from django.test import TestCase, TransactionTestCase - def get_order_number(test): - if hasattr(test, "cls") and test.cls: + def get_order_number(test: pytest.Item) -> int: + test_cls = getattr(test, "cls", None) + if test_cls: # Beware, TestCase is a subclass of TransactionTestCase - if issubclass(test.cls, TestCase): + if issubclass(test_cls, TestCase): return 0 - if issubclass(test.cls, TransactionTestCase): + if issubclass(test_cls, TransactionTestCase): return 1 marker_db = test.get_closest_marker('django_db') - if marker_db: + if not marker_db: + transaction = None + else: transaction = validate_django_db(marker_db)[0] if transaction is True: return 1 - else: - transaction = None fixtures = getattr(test, 'fixturenames', []) if "transactional_db" in fixtures: @@ -376,7 +396,7 @@ def get_order_number(test): @pytest.fixture(autouse=True, scope="session") -def django_test_environment(request): +def django_test_environment(request) -> None: """ Ensure that Django is loaded and has its testing environment setup. @@ -402,7 +422,7 @@ def django_test_environment(request): @pytest.fixture(scope="session") -def django_db_blocker(): +def django_db_blocker() -> "Optional[_DatabaseBlocker]": """Wrapper around Django's database access. This object can be used to re-enable database access. This fixture is used @@ -422,7 +442,7 @@ def django_db_blocker(): @pytest.fixture(autouse=True) -def _django_db_marker(request): +def _django_db_marker(request) -> None: """Implement the django_db marker, internal to pytest-django. This will dynamically request the ``db``, ``transactional_db`` or @@ -440,7 +460,10 @@ def _django_db_marker(request): @pytest.fixture(autouse=True, scope="class") -def _django_setup_unittest(request, django_db_blocker): +def _django_setup_unittest( + request, + django_db_blocker: "_DatabaseBlocker", +) -> Generator[None, None, None]: """Setup a django unittest, internal to pytest-django.""" if not django_settings_is_configured() or not is_django_unittest(request): yield @@ -452,22 +475,22 @@ def _django_setup_unittest(request, django_db_blocker): from _pytest.unittest import TestCaseFunction original_runtest = TestCaseFunction.runtest - def non_debugging_runtest(self): + def non_debugging_runtest(self) -> None: self._testcase(result=self) try: - TestCaseFunction.runtest = non_debugging_runtest + TestCaseFunction.runtest = non_debugging_runtest # type: ignore[assignment] request.getfixturevalue("django_db_setup") with django_db_blocker.unblock(): yield finally: - TestCaseFunction.runtest = original_runtest + TestCaseFunction.runtest = original_runtest # type: ignore[assignment] @pytest.fixture(scope="function", autouse=True) -def _dj_autoclear_mailbox(): +def _dj_autoclear_mailbox() -> None: if not django_settings_is_configured(): return @@ -477,9 +500,12 @@ def _dj_autoclear_mailbox(): @pytest.fixture(scope="function") -def mailoutbox(django_mail_patch_dns, _dj_autoclear_mailbox): +def mailoutbox( + django_mail_patch_dns: None, + _dj_autoclear_mailbox: None, +) -> "Optional[List[django.core.mail.EmailMessage]]": if not django_settings_is_configured(): - return + return None from django.core import mail @@ -487,19 +513,22 @@ def mailoutbox(django_mail_patch_dns, _dj_autoclear_mailbox): @pytest.fixture(scope="function") -def django_mail_patch_dns(monkeypatch, django_mail_dnsname): +def django_mail_patch_dns( + monkeypatch, + django_mail_dnsname: str, +) -> None: from django.core import mail monkeypatch.setattr(mail.message, "DNS_NAME", django_mail_dnsname) @pytest.fixture(scope="function") -def django_mail_dnsname(): +def django_mail_dnsname() -> str: return "fake-tests.example.com" @pytest.fixture(autouse=True, scope="function") -def _django_set_urlconf(request): +def _django_set_urlconf(request) -> None: """Apply the @pytest.mark.urls marker, internal to pytest-django.""" marker = request.node.get_closest_marker("urls") if marker: @@ -513,7 +542,7 @@ def _django_set_urlconf(request): clear_url_caches() set_urlconf(None) - def restore(): + def restore() -> None: django.conf.settings.ROOT_URLCONF = original_urlconf # Copy the pattern from # https://github.com/django/django/blob/main/django/test/signals.py#L152 @@ -541,10 +570,10 @@ def _fail_for_invalid_template_variable(): class InvalidVarException: """Custom handler for invalid strings in templates.""" - def __init__(self): + def __init__(self) -> None: self.fail = True - def __contains__(self, key): + def __contains__(self, key: str) -> bool: return key == "%s" @staticmethod @@ -567,11 +596,11 @@ def _get_origin(): from django.template import Template # finding the ``render`` needle in the stack - frame = reduce( + frameinfo = reduce( lambda x, y: y[3] == "render" and "base.py" in y[1] and y or x, stack ) # assert 0, stack - frame = frame[0] + frame = frameinfo[0] # finding only the frame locals in all frame members f_locals = reduce( lambda x, y: y[0] == "f_locals" and y or x, inspect.getmembers(frame) @@ -581,7 +610,7 @@ def _get_origin(): if isinstance(template, Template): return template.name - def __mod__(self, var): + def __mod__(self, var: str) -> str: origin = self._get_origin() if origin: msg = "Undefined template variable '{}' in '{}'".format(var, origin) @@ -603,7 +632,7 @@ def __mod__(self, var): @pytest.fixture(autouse=True) -def _template_string_if_invalid_marker(request): +def _template_string_if_invalid_marker(request) -> None: """Apply the @pytest.mark.ignore_template_errors marker, internal to pytest-django.""" marker = request.keywords.get("ignore_template_errors", None) @@ -616,7 +645,7 @@ def _template_string_if_invalid_marker(request): @pytest.fixture(autouse=True, scope="function") -def _django_clear_site_cache(): +def _django_clear_site_cache() -> None: """Clears ``django.contrib.sites.models.SITE_CACHE`` to avoid unexpected behavior with cached site objects. """ @@ -634,13 +663,13 @@ def _django_clear_site_cache(): class _DatabaseBlockerContextManager: - def __init__(self, db_blocker): + def __init__(self, db_blocker) -> None: self._db_blocker = db_blocker - def __enter__(self): + def __enter__(self) -> None: pass - def __exit__(self, exc_type, exc_value, traceback): + def __exit__(self, exc_type, exc_value, traceback) -> None: self._db_blocker.restore() @@ -655,7 +684,7 @@ def __init__(self): self._real_ensure_connection = None @property - def _dj_db_wrapper(self): + def _dj_db_wrapper(self) -> "django.db.backends.base.base.BaseDatabaseWrapper": from django.db.backends.base.base import BaseDatabaseWrapper # The first time the _dj_db_wrapper is accessed, we will save a @@ -665,10 +694,10 @@ def _dj_db_wrapper(self): return BaseDatabaseWrapper - def _save_active_wrapper(self): - return self._history.append(self._dj_db_wrapper.ensure_connection) + def _save_active_wrapper(self) -> None: + self._history.append(self._dj_db_wrapper.ensure_connection) - def _blocking_wrapper(*args, **kwargs): + def _blocking_wrapper(*args, **kwargs) -> "NoReturn": __tracebackhide__ = True __tracebackhide__ # Silence pyflakes raise RuntimeError( @@ -677,26 +706,26 @@ def _blocking_wrapper(*args, **kwargs): '"db" or "transactional_db" fixtures to enable it.' ) - def unblock(self): + def unblock(self) -> "ContextManager[None]": """Enable access to the Django database.""" self._save_active_wrapper() self._dj_db_wrapper.ensure_connection = self._real_ensure_connection return _DatabaseBlockerContextManager(self) - def block(self): + def block(self) -> "ContextManager[None]": """Disable access to the Django database.""" self._save_active_wrapper() self._dj_db_wrapper.ensure_connection = self._blocking_wrapper return _DatabaseBlockerContextManager(self) - def restore(self): + def restore(self) -> None: self._dj_db_wrapper.ensure_connection = self._history.pop() _blocking_manager = _DatabaseBlocker() -def validate_django_db(marker): +def validate_django_db(marker) -> Tuple[bool, bool]: """Validate the django_db marker. It checks the signature and creates the ``transaction`` and @@ -706,20 +735,23 @@ def validate_django_db(marker): A sequence reset is only allowed when combined with a transaction. """ - def apifun(transaction=False, reset_sequences=False): + def apifun( + transaction: bool = False, + reset_sequences: bool = False, + ) -> Tuple[bool, bool]: return transaction, reset_sequences return apifun(*marker.args, **marker.kwargs) -def validate_urls(marker): +def validate_urls(marker) -> List[str]: """Validate the urls marker. It checks the signature and creates the `urls` attribute on the marker which will have the correct value. """ - def apifun(urls): + def apifun(urls: List[str]) -> List[str]: return urls return apifun(*marker.args, **marker.kwargs) diff --git a/pytest_django_test/app/models.py b/pytest_django_test/app/models.py index 381ce30aa..804d36020 100644 --- a/pytest_django_test/app/models.py +++ b/pytest_django_test/app/models.py @@ -2,4 +2,4 @@ class Item(models.Model): - name = models.CharField(max_length=100) + name = models.CharField(max_length=100) # type: str diff --git a/pytest_django_test/app/views.py b/pytest_django_test/app/views.py index b400f408b..72b463569 100644 --- a/pytest_django_test/app/views.py +++ b/pytest_django_test/app/views.py @@ -1,14 +1,14 @@ -from django.http import HttpResponse +from django.http import HttpRequest, HttpResponse from django.template import Template from django.template.context import Context from .models import Item -def admin_required_view(request): +def admin_required_view(request: HttpRequest) -> HttpResponse: assert request.user.is_staff return HttpResponse(Template("You are an admin").render(Context())) -def item_count(request): +def item_count(request: HttpRequest) -> HttpResponse: return HttpResponse("Item count: %d" % Item.objects.count()) diff --git a/setup.cfg b/setup.cfg index 24d1cbf62..7089af2d0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -71,6 +71,7 @@ exclude = lib/,src/,docs/,bin/ forced_separate = tests,pytest_django,pytest_django_test [mypy] +check_untyped_defs = True disallow_any_generics = True no_implicit_optional = True show_error_codes = True diff --git a/tests/conftest.py b/tests/conftest.py index 7a8ec937c..1e7878a5e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +from typing import Optional import copy import shutil from textwrap import dedent @@ -12,13 +13,17 @@ REPOSITORY_ROOT = pathlib.Path(__file__).parent -def pytest_configure(config): +def pytest_configure(config) -> None: config.addinivalue_line( "markers", "django_project: options for the django_testdir fixture" ) -def _marker_apifun(extra_settings="", create_manage_py=False, project_root=None): +def _marker_apifun( + extra_settings: str = "", + create_manage_py: bool = False, + project_root: Optional[str] = None, +): return { "extra_settings": extra_settings, "create_manage_py": create_manage_py, @@ -116,12 +121,12 @@ def django_testdir(request, testdir, monkeypatch): monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "tpkg.the_settings") - def create_test_module(test_code, filename="test_the_test.py"): + def create_test_module(test_code: str, filename: str = "test_the_test.py"): r = tpkg_path.join(filename) r.write(dedent(test_code), ensure=True) return r - def create_app_file(code, filename): + def create_app_file(code: str, filename: str): r = test_app_path.join(filename) r.write(dedent(code), ensure=True) return r diff --git a/tests/test_asserts.py b/tests/test_asserts.py index 578fb05ab..01b3b0603 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -1,6 +1,7 @@ """ Tests the dynamic loading of all Django assertion cases. """ +from typing import List import inspect import pytest @@ -9,7 +10,7 @@ from pytest_django.asserts import __all__ as asserts_all -def _get_actual_assertions_names(): +def _get_actual_assertions_names() -> List[str]: """ Returns list with names of all assertion helpers in Django. """ @@ -18,7 +19,7 @@ def _get_actual_assertions_names(): obj = DjangoTestCase('run') - def is_assert(func): + def is_assert(func) -> bool: return func.startswith('assert') and '_' not in func base_methods = [name for name, member in @@ -29,7 +30,7 @@ def is_assert(func): if is_assert(name) and name not in base_methods] -def test_django_asserts_available(): +def test_django_asserts_available() -> None: django_assertions = _get_actual_assertions_names() expected_assertions = asserts_all assert set(django_assertions) == set(expected_assertions) @@ -39,7 +40,7 @@ def test_django_asserts_available(): @pytest.mark.django_db -def test_sanity(): +def test_sanity() -> None: from django.http import HttpResponse from pytest_django.asserts import assertContains, assertNumQueries diff --git a/tests/test_database.py b/tests/test_database.py index 2607e1915..7ad5f599b 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -6,7 +6,7 @@ from pytest_django_test.app.models import Item -def db_supports_reset_sequences(): +def db_supports_reset_sequences() -> bool: """Return if the current db engine supports `reset_sequences`.""" return ( connection.features.supports_transactions @@ -14,7 +14,7 @@ def db_supports_reset_sequences(): ) -def test_noaccess(): +def test_noaccess() -> None: with pytest.raises(RuntimeError): Item.objects.create(name="spam") with pytest.raises(RuntimeError): @@ -22,20 +22,20 @@ def test_noaccess(): @pytest.fixture -def noaccess(): +def noaccess() -> None: with pytest.raises(RuntimeError): Item.objects.create(name="spam") with pytest.raises(RuntimeError): Item.objects.count() -def test_noaccess_fixture(noaccess): +def test_noaccess_fixture(noaccess: None) -> None: # Setup will fail if this test needs to fail pass @pytest.fixture -def non_zero_sequences_counter(db): +def non_zero_sequences_counter(db: None) -> None: """Ensure that the db's internal sequence counter is > 1. This is used to test the `reset_sequences` feature. @@ -50,7 +50,7 @@ class TestDatabaseFixtures: """Tests for the different database fixtures.""" @pytest.fixture(params=["db", "transactional_db", "django_db_reset_sequences"]) - def all_dbs(self, request): + def all_dbs(self, request) -> None: if request.param == "django_db_reset_sequences": return request.getfixturevalue("django_db_reset_sequences") elif request.param == "transactional_db": @@ -58,34 +58,36 @@ def all_dbs(self, request): elif request.param == "db": return request.getfixturevalue("db") - def test_access(self, all_dbs): + def test_access(self, all_dbs: None) -> None: Item.objects.create(name="spam") - def test_clean_db(self, all_dbs): + def test_clean_db(self, all_dbs: None) -> None: # Relies on the order: test_access created an object assert Item.objects.count() == 0 - def test_transactions_disabled(self, db): + def test_transactions_disabled(self, db: None) -> None: if not connections_support_transactions(): pytest.skip("transactions required for this test") assert connection.in_atomic_block - def test_transactions_enabled(self, transactional_db): + def test_transactions_enabled(self, transactional_db: None) -> None: if not connections_support_transactions(): pytest.skip("transactions required for this test") assert not connection.in_atomic_block - def test_transactions_enabled_via_reset_seq(self, django_db_reset_sequences): + def test_transactions_enabled_via_reset_seq( + self, django_db_reset_sequences: None, + ) -> None: if not connections_support_transactions(): pytest.skip("transactions required for this test") assert not connection.in_atomic_block def test_django_db_reset_sequences_fixture( - self, db, django_testdir, non_zero_sequences_counter - ): + self, db: None, django_testdir, non_zero_sequences_counter: None, + ) -> None: if not db_supports_reset_sequences(): pytest.skip( @@ -113,11 +115,11 @@ def test_django_db_reset_sequences_requested( ) @pytest.fixture - def mydb(self, all_dbs): + def mydb(self, all_dbs: None) -> None: # This fixture must be able to access the database Item.objects.create(name="spam") - def test_mydb(self, mydb): + def test_mydb(self, mydb: None) -> None: if not connections_support_transactions(): pytest.skip("transactions required for this test") @@ -125,22 +127,22 @@ def test_mydb(self, mydb): item = Item.objects.get(name="spam") assert item - def test_fixture_clean(self, all_dbs): + def test_fixture_clean(self, all_dbs: None) -> None: # Relies on the order: test_mydb created an object # See https://github.com/pytest-dev/pytest-django/issues/17 assert Item.objects.count() == 0 @pytest.fixture - def fin(self, request, all_dbs): + def fin(self, request, all_dbs: None) -> None: # This finalizer must be able to access the database request.addfinalizer(lambda: Item.objects.create(name="spam")) - def test_fin(self, fin): + def test_fin(self, fin: None) -> None: # Check finalizer has db access (teardown will fail if not) pass @pytest.mark.skipif(get_django_version() < (3, 2), reason="Django >= 3.2 required") - def test_durable_transactions(self, all_dbs): + def test_durable_transactions(self, all_dbs: None) -> None: with transaction.atomic(durable=True): item = Item.objects.create(name="foo") assert Item.objects.get() == item @@ -148,32 +150,35 @@ def test_durable_transactions(self, all_dbs): class TestDatabaseFixturesAllOrder: @pytest.fixture - def fixture_with_db(self, db): + def fixture_with_db(self, db: None) -> None: Item.objects.create(name="spam") @pytest.fixture - def fixture_with_transdb(self, transactional_db): + def fixture_with_transdb(self, transactional_db: None) -> None: Item.objects.create(name="spam") @pytest.fixture - def fixture_with_reset_sequences(self, django_db_reset_sequences): + def fixture_with_reset_sequences(self, django_db_reset_sequences: None) -> None: Item.objects.create(name="spam") - def test_trans(self, fixture_with_transdb): + def test_trans(self, fixture_with_transdb: None) -> None: pass - def test_db(self, fixture_with_db): + def test_db(self, fixture_with_db: None) -> None: pass - def test_db_trans(self, fixture_with_db, fixture_with_transdb): + def test_db_trans(self, fixture_with_db: None, fixture_with_transdb: None) -> None: pass - def test_trans_db(self, fixture_with_transdb, fixture_with_db): + def test_trans_db(self, fixture_with_transdb: None, fixture_with_db: None) -> None: pass def test_reset_sequences( - self, fixture_with_reset_sequences, fixture_with_transdb, fixture_with_db - ): + self, + fixture_with_reset_sequences: None, + fixture_with_transdb: None, + fixture_with_db: None, + ) -> None: pass @@ -181,47 +186,47 @@ class TestDatabaseMarker: "Tests for the django_db marker." @pytest.mark.django_db - def test_access(self): + def test_access(self) -> None: Item.objects.create(name="spam") @pytest.mark.django_db - def test_clean_db(self): + def test_clean_db(self) -> None: # Relies on the order: test_access created an object. assert Item.objects.count() == 0 @pytest.mark.django_db - def test_transactions_disabled(self): + def test_transactions_disabled(self) -> None: if not connections_support_transactions(): pytest.skip("transactions required for this test") assert connection.in_atomic_block @pytest.mark.django_db(transaction=False) - def test_transactions_disabled_explicit(self): + def test_transactions_disabled_explicit(self) -> None: if not connections_support_transactions(): pytest.skip("transactions required for this test") assert connection.in_atomic_block @pytest.mark.django_db(transaction=True) - def test_transactions_enabled(self): + def test_transactions_enabled(self) -> None: if not connections_support_transactions(): pytest.skip("transactions required for this test") assert not connection.in_atomic_block @pytest.mark.django_db - def test_reset_sequences_disabled(self, request): + def test_reset_sequences_disabled(self, request) -> None: marker = request.node.get_closest_marker("django_db") assert not marker.kwargs @pytest.mark.django_db(reset_sequences=True) - def test_reset_sequences_enabled(self, request): + def test_reset_sequences_enabled(self, request) -> None: marker = request.node.get_closest_marker("django_db") assert marker.kwargs["reset_sequences"] -def test_unittest_interaction(django_testdir): +def test_unittest_interaction(django_testdir) -> None: "Test that (non-Django) unittests cannot access the DB." django_testdir.create_test_module( @@ -266,7 +271,7 @@ def test_db_access_3(self): class Test_database_blocking: - def test_db_access_in_conftest(self, django_testdir): + def test_db_access_in_conftest(self, django_testdir) -> None: """Make sure database access in conftest module is prohibited.""" django_testdir.makeconftest( @@ -284,7 +289,7 @@ def test_db_access_in_conftest(self, django_testdir): ] ) - def test_db_access_in_test_module(self, django_testdir): + def test_db_access_in_test_module(self, django_testdir) -> None: django_testdir.create_test_module( """ from tpkg.app.models import Item diff --git a/tests/test_db_access_in_repr.py b/tests/test_db_access_in_repr.py index c8511cf17..64ae4132f 100644 --- a/tests/test_db_access_in_repr.py +++ b/tests/test_db_access_in_repr.py @@ -1,4 +1,4 @@ -def test_db_access_with_repr_in_report(django_testdir): +def test_db_access_with_repr_in_report(django_testdir) -> None: django_testdir.create_test_module( """ import pytest diff --git a/tests/test_db_setup.py b/tests/test_db_setup.py index 21e065948..d8d339c01 100644 --- a/tests/test_db_setup.py +++ b/tests/test_db_setup.py @@ -9,7 +9,7 @@ ) -def test_db_reuse_simple(django_testdir): +def test_db_reuse_simple(django_testdir) -> None: "A test for all backends to check that `--reuse-db` works." django_testdir.create_test_module( """ @@ -28,7 +28,7 @@ def test_db_can_be_accessed(): result.stdout.fnmatch_lines(["*test_db_can_be_accessed PASSED*"]) -def test_db_order(django_testdir): +def test_db_order(django_testdir) -> None: """Test order in which tests are being executed.""" django_testdir.create_test_module(''' @@ -82,7 +82,7 @@ def test_run_second_transaction_test_case(self): ]) -def test_db_reuse(django_testdir): +def test_db_reuse(django_testdir) -> None: """ Test the re-use db functionality. """ @@ -144,7 +144,7 @@ class TestSqlite: } } - def test_sqlite_test_name_used(self, django_testdir): + def test_sqlite_test_name_used(self, django_testdir) -> None: django_testdir.create_test_module( """ @@ -167,7 +167,7 @@ def test_a(): result.stdout.fnmatch_lines(["*test_a*PASSED*"]) -def test_xdist_with_reuse(django_testdir): +def test_xdist_with_reuse(django_testdir) -> None: pytest.importorskip("xdist") skip_if_sqlite_in_memory() @@ -251,7 +251,7 @@ class TestSqliteWithXdist: } } - def test_sqlite_in_memory_used(self, django_testdir): + def test_sqlite_in_memory_used(self, django_testdir) -> None: pytest.importorskip("xdist") django_testdir.create_test_module( @@ -288,7 +288,7 @@ class TestSqliteWithMultipleDbsAndXdist: } } - def test_sqlite_database_renamed(self, django_testdir): + def test_sqlite_database_renamed(self, django_testdir) -> None: pytest.importorskip("xdist") django_testdir.create_test_module( @@ -334,7 +334,7 @@ class TestSqliteWithTox: } } - def test_db_with_tox_suffix(self, django_testdir, monkeypatch): + def test_db_with_tox_suffix(self, django_testdir, monkeypatch) -> None: "A test to check that Tox DB suffix works when running in parallel." monkeypatch.setenv("TOX_PARALLEL_ENV", "py37-django22") @@ -358,7 +358,7 @@ def test_inner(): assert result.ret == 0 result.stdout.fnmatch_lines(["*test_inner*PASSED*"]) - def test_db_with_empty_tox_suffix(self, django_testdir, monkeypatch): + def test_db_with_empty_tox_suffix(self, django_testdir, monkeypatch) -> None: "A test to check that Tox DB suffix is not used when suffix would be empty." monkeypatch.setenv("TOX_PARALLEL_ENV", "") @@ -393,7 +393,7 @@ class TestSqliteWithToxAndXdist: } } - def test_db_with_tox_suffix(self, django_testdir, monkeypatch): + def test_db_with_tox_suffix(self, django_testdir, monkeypatch) -> None: "A test to check that both Tox and xdist suffixes work together." pytest.importorskip("xdist") monkeypatch.setenv("TOX_PARALLEL_ENV", "py37-django22") @@ -429,7 +429,7 @@ class TestSqliteInMemoryWithXdist: } } - def test_sqlite_in_memory_used(self, django_testdir): + def test_sqlite_in_memory_used(self, django_testdir) -> None: pytest.importorskip("xdist") django_testdir.create_test_module( @@ -455,7 +455,7 @@ def test_a(): class TestNativeMigrations: """ Tests for Django Migrations """ - def test_no_migrations(self, django_testdir): + def test_no_migrations(self, django_testdir) -> None: django_testdir.create_test_module( """ import pytest @@ -482,7 +482,7 @@ def test_inner_migrations(): assert "Operations to perform:" not in result.stdout.str() result.stdout.fnmatch_lines(["*= 1 passed*"]) - def test_migrations_run(self, django_testdir): + def test_migrations_run(self, django_testdir) -> None: testdir = django_testdir testdir.create_test_module( """ diff --git a/tests/test_django_configurations.py b/tests/test_django_configurations.py index 70c0126ab..d5941b8e9 100644 --- a/tests/test_django_configurations.py +++ b/tests/test_django_configurations.py @@ -23,7 +23,7 @@ class MySettings(Configuration): """ -def test_dc_env(testdir, monkeypatch): +def test_dc_env(testdir, monkeypatch) -> None: monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "tpkg.settings_env") monkeypatch.setenv("DJANGO_CONFIGURATION", "MySettings") @@ -47,7 +47,7 @@ def test_settings(): assert result.ret == 0 -def test_dc_env_overrides_ini(testdir, monkeypatch): +def test_dc_env_overrides_ini(testdir, monkeypatch) -> None: monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "tpkg.settings_env") monkeypatch.setenv("DJANGO_CONFIGURATION", "MySettings") @@ -78,7 +78,7 @@ def test_ds(): assert result.ret == 0 -def test_dc_ini(testdir, monkeypatch): +def test_dc_ini(testdir, monkeypatch) -> None: monkeypatch.delenv("DJANGO_SETTINGS_MODULE") testdir.makeini( @@ -108,7 +108,7 @@ def test_ds(): assert result.ret == 0 -def test_dc_option(testdir, monkeypatch): +def test_dc_option(testdir, monkeypatch) -> None: monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "DO_NOT_USE_env") monkeypatch.setenv("DJANGO_CONFIGURATION", "DO_NOT_USE_env") diff --git a/tests/test_django_settings_module.py b/tests/test_django_settings_module.py index 9040b522e..fb008e12f 100644 --- a/tests/test_django_settings_module.py +++ b/tests/test_django_settings_module.py @@ -18,7 +18,7 @@ """ -def test_ds_ini(testdir, monkeypatch): +def test_ds_ini(testdir, monkeypatch) -> None: monkeypatch.delenv("DJANGO_SETTINGS_MODULE") testdir.makeini( """ @@ -44,7 +44,7 @@ def test_ds(): assert result.ret == 0 -def test_ds_env(testdir, monkeypatch): +def test_ds_env(testdir, monkeypatch) -> None: monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "tpkg.settings_env") pkg = testdir.mkpydir("tpkg") settings = pkg.join("settings_env.py") @@ -64,7 +64,7 @@ def test_settings(): ]) -def test_ds_option(testdir, monkeypatch): +def test_ds_option(testdir, monkeypatch) -> None: monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "DO_NOT_USE_env") testdir.makeini( """ @@ -90,7 +90,7 @@ def test_ds(): ]) -def test_ds_env_override_ini(testdir, monkeypatch): +def test_ds_env_override_ini(testdir, monkeypatch) -> None: "DSM env should override ini." monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "tpkg.settings_env") testdir.makeini( @@ -115,7 +115,7 @@ def test_ds(): assert result.ret == 0 -def test_ds_non_existent(testdir, monkeypatch): +def test_ds_non_existent(testdir, monkeypatch) -> None: """ Make sure we do not fail with INTERNALERROR if an incorrect DJANGO_SETTINGS_MODULE is given. @@ -127,7 +127,7 @@ def test_ds_non_existent(testdir, monkeypatch): assert result.ret != 0 -def test_ds_after_user_conftest(testdir, monkeypatch): +def test_ds_after_user_conftest(testdir, monkeypatch) -> None: """ Test that the settings module can be imported, after pytest has adjusted the sys.path. @@ -141,7 +141,7 @@ def test_ds_after_user_conftest(testdir, monkeypatch): assert result.ret == 0 -def test_ds_in_pytest_configure(testdir, monkeypatch): +def test_ds_in_pytest_configure(testdir, monkeypatch) -> None: monkeypatch.delenv("DJANGO_SETTINGS_MODULE") pkg = testdir.mkpydir("tpkg") settings = pkg.join("settings_ds.py") @@ -171,7 +171,7 @@ def test_anything(): assert r.ret == 0 -def test_django_settings_configure(testdir, monkeypatch): +def test_django_settings_configure(testdir, monkeypatch) -> None: """ Make sure Django can be configured without setting DJANGO_SETTINGS_MODULE altogether, relying on calling @@ -228,7 +228,7 @@ def test_user_count(): result.stdout.fnmatch_lines(["* 4 passed*"]) -def test_settings_in_hook(testdir, monkeypatch): +def test_settings_in_hook(testdir, monkeypatch) -> None: monkeypatch.delenv("DJANGO_SETTINGS_MODULE") testdir.makeconftest( """ @@ -261,7 +261,7 @@ def test_user_count(): assert r.ret == 0 -def test_django_not_loaded_without_settings(testdir, monkeypatch): +def test_django_not_loaded_without_settings(testdir, monkeypatch) -> None: """ Make sure Django is not imported at all if no Django settings is specified. """ @@ -278,7 +278,7 @@ def test_settings(): assert result.ret == 0 -def test_debug_false_by_default(testdir, monkeypatch): +def test_debug_false_by_default(testdir, monkeypatch) -> None: monkeypatch.delenv("DJANGO_SETTINGS_MODULE") testdir.makeconftest( """ @@ -308,7 +308,7 @@ def test_debug_is_false(): @pytest.mark.parametrize('django_debug_mode', (False, True)) -def test_django_debug_mode_true_false(testdir, monkeypatch, django_debug_mode): +def test_django_debug_mode_true_false(testdir, monkeypatch, django_debug_mode: bool) -> None: monkeypatch.delenv("DJANGO_SETTINGS_MODULE") testdir.makeini( """ @@ -344,7 +344,7 @@ def test_debug_is_false(): @pytest.mark.parametrize('settings_debug', (False, True)) -def test_django_debug_mode_keep(testdir, monkeypatch, settings_debug): +def test_django_debug_mode_keep(testdir, monkeypatch, settings_debug: bool) -> None: monkeypatch.delenv("DJANGO_SETTINGS_MODULE") testdir.makeini( """ @@ -386,7 +386,7 @@ def test_debug_is_false(): ] """ ) -def test_django_setup_sequence(django_testdir): +def test_django_setup_sequence(django_testdir) -> None: django_testdir.create_app_file( """ from django.apps import apps, AppConfig @@ -434,7 +434,7 @@ def test_anything(): assert result.ret == 0 -def test_no_ds_but_django_imported(testdir, monkeypatch): +def test_no_ds_but_django_imported(testdir, monkeypatch) -> None: """pytest-django should not bail out, if "django" has been imported somewhere, e.g. via pytest-splinter.""" @@ -461,7 +461,7 @@ def test_cfg(pytestconfig): assert r.ret == 0 -def test_no_ds_but_django_conf_imported(testdir, monkeypatch): +def test_no_ds_but_django_conf_imported(testdir, monkeypatch) -> None: """pytest-django should not bail out, if "django.conf" has been imported somewhere, e.g. via hypothesis (#599).""" @@ -498,7 +498,7 @@ def test_cfg(pytestconfig): assert r.ret == 0 -def test_no_django_settings_but_django_imported(testdir, monkeypatch): +def test_no_django_settings_but_django_imported(testdir, monkeypatch) -> None: """Make sure we do not crash when Django happens to be imported, but settings is not properly configured""" monkeypatch.delenv("DJANGO_SETTINGS_MODULE") diff --git a/tests/test_environment.py b/tests/test_environment.py index 87e45f5ff..a237bd908 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -17,7 +17,7 @@ @pytest.mark.parametrize("subject", ["subject1", "subject2"]) -def test_autoclear_mailbox(subject): +def test_autoclear_mailbox(subject: str) -> None: assert len(mail.outbox) == 0 mail.send_mail(subject, "body", "from@example.com", ["to@example.com"]) assert len(mail.outbox) == 1 @@ -30,15 +30,15 @@ def test_autoclear_mailbox(subject): class TestDirectAccessWorksForDjangoTestCase(TestCase): - def _do_test(self): + def _do_test(self) -> None: assert len(mail.outbox) == 0 mail.send_mail("subject", "body", "from@example.com", ["to@example.com"]) assert len(mail.outbox) == 1 - def test_one(self): + def test_one(self) -> None: self._do_test() - def test_two(self): + def test_two(self) -> None: self._do_test() @@ -51,7 +51,7 @@ def test_two(self): ROOT_URLCONF = 'tpkg.app.urls' """ ) -def test_invalid_template_variable(django_testdir): +def test_invalid_template_variable(django_testdir) -> None: django_testdir.create_app_file( """ from django.urls import path @@ -112,7 +112,7 @@ def test_ignore(client): ROOT_URLCONF = 'tpkg.app.urls' """ ) -def test_invalid_template_with_default_if_none(django_testdir): +def test_invalid_template_with_default_if_none(django_testdir) -> None: django_testdir.create_app_file( """
{{ data.empty|default:'d' }}
@@ -154,7 +154,7 @@ def test_for_invalid_template(): ROOT_URLCONF = 'tpkg.app.urls' """ ) -def test_invalid_template_variable_opt_in(django_testdir): +def test_invalid_template_variable_opt_in(django_testdir) -> None: django_testdir.create_app_file( """ from django.urls import path @@ -195,24 +195,24 @@ def test_ignore(client): @pytest.mark.django_db -def test_database_rollback(): +def test_database_rollback() -> None: assert Item.objects.count() == 0 Item.objects.create(name="blah") assert Item.objects.count() == 1 @pytest.mark.django_db -def test_database_rollback_again(): +def test_database_rollback_again() -> None: test_database_rollback() @pytest.mark.django_db -def test_database_name(): +def test_database_name() -> None: dirname, name = os.path.split(connection.settings_dict["NAME"]) assert "file:memorydb" in name or name == ":memory:" or name.startswith("test_") -def test_database_noaccess(): +def test_database_noaccess() -> None: with pytest.raises(RuntimeError): Item.objects.count() @@ -235,17 +235,17 @@ def test_inner_testrunner(): ) return django_testdir - def test_default(self, testdir): + def test_default(self, testdir) -> None: """Not verbose by default.""" result = testdir.runpytest_subprocess("-s") result.stdout.fnmatch_lines(["tpkg/test_the_test.py .*"]) - def test_vq_verbosity_0(self, testdir): + def test_vq_verbosity_0(self, testdir) -> None: """-v and -q results in verbosity 0.""" result = testdir.runpytest_subprocess("-s", "-v", "-q") result.stdout.fnmatch_lines(["tpkg/test_the_test.py .*"]) - def test_verbose_with_v(self, testdir): + def test_verbose_with_v(self, testdir) -> None: """Verbose output with '-v'.""" result = testdir.runpytest_subprocess("-s", "-v") result.stdout.fnmatch_lines_random(["tpkg/test_the_test.py:*", "*PASSED*"]) @@ -253,7 +253,7 @@ def test_verbose_with_v(self, testdir): ["*Destroying test database for alias 'default'*"] ) - def test_more_verbose_with_vv(self, testdir): + def test_more_verbose_with_vv(self, testdir) -> None: """More verbose output with '-v -v'.""" result = testdir.runpytest_subprocess("-s", "-v", "-v") result.stdout.fnmatch_lines_random( @@ -271,7 +271,7 @@ def test_more_verbose_with_vv(self, testdir): ] ) - def test_more_verbose_with_vv_and_reusedb(self, testdir): + def test_more_verbose_with_vv_and_reusedb(self, testdir) -> None: """More verbose output with '-v -v', and --create-db.""" result = testdir.runpytest_subprocess("-s", "-v", "-v", "--create-db") result.stdout.fnmatch_lines(["tpkg/test_the_test.py:*", "*PASSED*"]) @@ -284,7 +284,7 @@ def test_more_verbose_with_vv_and_reusedb(self, testdir): @pytest.mark.django_db @pytest.mark.parametrize("site_name", ["site1", "site2"]) -def test_clear_site_cache(site_name, rf, monkeypatch): +def test_clear_site_cache(site_name: str, rf, monkeypatch) -> None: request = rf.get("/") monkeypatch.setattr(request, "get_host", lambda: "foo.com") Site.objects.create(domain="foo.com", name=site_name) @@ -293,7 +293,7 @@ def test_clear_site_cache(site_name, rf, monkeypatch): @pytest.mark.django_db @pytest.mark.parametrize("site_name", ["site1", "site2"]) -def test_clear_site_cache_check_site_cache_size(site_name, settings): +def test_clear_site_cache_check_site_cache_size(site_name: str, settings) -> None: assert len(site_models.SITE_CACHE) == 0 site = Site.objects.create(domain="foo.com", name=site_name) settings.SITE_ID = site.id diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index e20958761..d8e03adc5 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -3,6 +3,7 @@ Not quite all fixtures are tested here, the db and transactional_db fixtures are tested in test_database. """ +from typing import Generator import socket from contextlib import contextmanager from urllib.error import HTTPError @@ -21,7 +22,7 @@ @contextmanager -def nonverbose_config(config): +def nonverbose_config(config) -> Generator[None, None, None]: """Ensure that pytest's config.option.verbose is <= 0.""" if config.option.verbose <= 0: yield @@ -32,25 +33,25 @@ def nonverbose_config(config): config.option.verbose = saved -def test_client(client): +def test_client(client) -> None: assert isinstance(client, Client) @pytest.mark.skipif(get_django_version() < (3, 1), reason="Django >= 3.1 required") -def test_async_client(async_client): +def test_async_client(async_client) -> None: from django.test.client import AsyncClient assert isinstance(async_client, AsyncClient) @pytest.mark.django_db -def test_admin_client(admin_client): +def test_admin_client(admin_client: Client) -> None: assert isinstance(admin_client, Client) resp = admin_client.get("/admin-required/") assert force_str(resp.content) == "You are an admin" -def test_admin_client_no_db_marker(admin_client): +def test_admin_client_no_db_marker(admin_client: Client) -> None: assert isinstance(admin_client, Client) resp = admin_client.get("/admin-required/") assert force_str(resp.content) == "You are an admin" @@ -62,33 +63,38 @@ def existing_admin_user(django_user_model): return django_user_model._default_manager.create_superuser('admin', None, None) -def test_admin_client_existing_user(db, existing_admin_user, admin_user, admin_client): +def test_admin_client_existing_user( + db: None, + existing_admin_user, + admin_user, + admin_client: Client, +) -> None: resp = admin_client.get("/admin-required/") assert force_str(resp.content) == "You are an admin" @pytest.mark.django_db -def test_admin_user(admin_user, django_user_model): +def test_admin_user(admin_user, django_user_model) -> None: assert isinstance(admin_user, django_user_model) -def test_admin_user_no_db_marker(admin_user, django_user_model): +def test_admin_user_no_db_marker(admin_user, django_user_model) -> None: assert isinstance(admin_user, django_user_model) -def test_rf(rf): +def test_rf(rf) -> None: assert isinstance(rf, RequestFactory) @pytest.mark.skipif(get_django_version() < (3, 1), reason="Django >= 3.1 required") -def test_async_rf(async_rf): +def test_async_rf(async_rf) -> None: from django.test.client import AsyncRequestFactory assert isinstance(async_rf, AsyncRequestFactory) @pytest.mark.django_db -def test_django_assert_num_queries_db(request, django_assert_num_queries): +def test_django_assert_num_queries_db(request, django_assert_num_queries) -> None: with nonverbose_config(request.config): with django_assert_num_queries(3): Item.objects.create(name="foo") @@ -106,7 +112,7 @@ def test_django_assert_num_queries_db(request, django_assert_num_queries): @pytest.mark.django_db -def test_django_assert_max_num_queries_db(request, django_assert_max_num_queries): +def test_django_assert_max_num_queries_db(request, django_assert_max_num_queries) -> None: with nonverbose_config(request.config): with django_assert_max_num_queries(2): Item.objects.create(name="1-foo") @@ -128,8 +134,8 @@ def test_django_assert_max_num_queries_db(request, django_assert_max_num_queries @pytest.mark.django_db(transaction=True) def test_django_assert_num_queries_transactional_db( - request, transactional_db, django_assert_num_queries -): + request, transactional_db: None, django_assert_num_queries +) -> None: with nonverbose_config(request.config): with transaction.atomic(): with django_assert_num_queries(3): @@ -142,7 +148,7 @@ def test_django_assert_num_queries_transactional_db( Item.objects.create(name="quux") -def test_django_assert_num_queries_output(django_testdir): +def test_django_assert_num_queries_output(django_testdir) -> None: django_testdir.create_test_module( """ from django.contrib.contenttypes.models import ContentType @@ -160,7 +166,7 @@ def test_queries(django_assert_num_queries): assert result.ret == 1 -def test_django_assert_num_queries_output_verbose(django_testdir): +def test_django_assert_num_queries_output_verbose(django_testdir) -> None: django_testdir.create_test_module( """ from django.contrib.contenttypes.models import ContentType @@ -181,7 +187,7 @@ def test_queries(django_assert_num_queries): @pytest.mark.django_db -def test_django_assert_num_queries_db_connection(django_assert_num_queries): +def test_django_assert_num_queries_db_connection(django_assert_num_queries) -> None: from django.db import connection with django_assert_num_queries(1, connection=connection): @@ -196,7 +202,7 @@ def test_django_assert_num_queries_db_connection(django_assert_num_queries): @pytest.mark.django_db -def test_django_assert_num_queries_output_info(django_testdir): +def test_django_assert_num_queries_output_info(django_testdir) -> None: django_testdir.create_test_module( """ from django.contrib.contenttypes.models import ContentType @@ -228,40 +234,40 @@ def test_queries(django_assert_num_queries): class TestSettings: """Tests for the settings fixture, order matters""" - def test_modify_existing(self, settings): + def test_modify_existing(self, settings) -> None: assert settings.SECRET_KEY == "foobar" assert real_settings.SECRET_KEY == "foobar" settings.SECRET_KEY = "spam" assert settings.SECRET_KEY == "spam" assert real_settings.SECRET_KEY == "spam" - def test_modify_existing_again(self, settings): + def test_modify_existing_again(self, settings) -> None: assert settings.SECRET_KEY == "foobar" assert real_settings.SECRET_KEY == "foobar" - def test_new(self, settings): + def test_new(self, settings) -> None: assert not hasattr(settings, "SPAM") assert not hasattr(real_settings, "SPAM") settings.SPAM = "ham" assert settings.SPAM == "ham" assert real_settings.SPAM == "ham" - def test_new_again(self, settings): + def test_new_again(self, settings) -> None: assert not hasattr(settings, "SPAM") assert not hasattr(real_settings, "SPAM") - def test_deleted(self, settings): + def test_deleted(self, settings) -> None: assert hasattr(settings, "SECRET_KEY") assert hasattr(real_settings, "SECRET_KEY") del settings.SECRET_KEY assert not hasattr(settings, "SECRET_KEY") assert not hasattr(real_settings, "SECRET_KEY") - def test_deleted_again(self, settings): + def test_deleted_again(self, settings) -> None: assert hasattr(settings, "SECRET_KEY") assert hasattr(real_settings, "SECRET_KEY") - def test_signals(self, settings): + def test_signals(self, settings) -> None: result = [] def assert_signal(signal, sender, setting, value, enter): @@ -283,7 +289,7 @@ def assert_signal(signal, sender, setting, value, enter): settings.FOOBAR = "abc123" assert sorted(result) == [("FOOBAR", "abc123", True)] - def test_modification_signal(self, django_testdir): + def test_modification_signal(self, django_testdir) -> None: django_testdir.create_test_module( """ import pytest @@ -341,77 +347,77 @@ def test_set_non_existent(settings): class TestLiveServer: - def test_settings_before(self): + def test_settings_before(self) -> None: from django.conf import settings assert ( "{}.{}".format(settings.__class__.__module__, settings.__class__.__name__) == "django.conf.Settings" ) - TestLiveServer._test_settings_before_run = True + TestLiveServer._test_settings_before_run = True # type: ignore[attr-defined] - def test_url(self, live_server): + def test_url(self, live_server) -> None: assert live_server.url == force_str(live_server) - def test_change_settings(self, live_server, settings): + def test_change_settings(self, live_server, settings) -> None: assert live_server.url == force_str(live_server) - def test_settings_restored(self): + def test_settings_restored(self) -> None: """Ensure that settings are restored after test_settings_before.""" from django.conf import settings - assert TestLiveServer._test_settings_before_run is True + assert TestLiveServer._test_settings_before_run is True # type: ignore[attr-defined] assert ( "{}.{}".format(settings.__class__.__module__, settings.__class__.__name__) == "django.conf.Settings" ) assert settings.ALLOWED_HOSTS == ["testserver"] - def test_transactions(self, live_server): + def test_transactions(self, live_server) -> None: if not connections_support_transactions(): pytest.skip("transactions required for this test") assert not connection.in_atomic_block - def test_db_changes_visibility(self, live_server): + def test_db_changes_visibility(self, live_server) -> None: response_data = urlopen(live_server + "/item_count/").read() assert force_str(response_data) == "Item count: 0" Item.objects.create(name="foo") response_data = urlopen(live_server + "/item_count/").read() assert force_str(response_data) == "Item count: 1" - def test_fixture_db(self, db, live_server): + def test_fixture_db(self, db: None, live_server) -> None: Item.objects.create(name="foo") response_data = urlopen(live_server + "/item_count/").read() assert force_str(response_data) == "Item count: 1" - def test_fixture_transactional_db(self, transactional_db, live_server): + def test_fixture_transactional_db(self, transactional_db: None, live_server) -> None: Item.objects.create(name="foo") response_data = urlopen(live_server + "/item_count/").read() assert force_str(response_data) == "Item count: 1" @pytest.fixture - def item(self): + def item(self) -> None: # This has not requested database access explicitly, but the # live_server fixture auto-uses the transactional_db fixture. Item.objects.create(name="foo") - def test_item(self, item, live_server): + def test_item(self, item, live_server) -> None: pass @pytest.fixture - def item_db(self, db): + def item_db(self, db: None) -> Item: return Item.objects.create(name="foo") - def test_item_db(self, item_db, live_server): + def test_item_db(self, item_db: Item, live_server) -> None: response_data = urlopen(live_server + "/item_count/").read() assert force_str(response_data) == "Item count: 1" @pytest.fixture - def item_transactional_db(self, transactional_db): + def item_transactional_db(self, transactional_db: None) -> Item: return Item.objects.create(name="foo") - def test_item_transactional_db(self, item_transactional_db, live_server): + def test_item_transactional_db(self, item_transactional_db: Item, live_server) -> None: response_data = urlopen(live_server + "/item_count/").read() assert force_str(response_data) == "Item count: 1" @@ -429,7 +435,7 @@ def test_item_transactional_db(self, item_transactional_db, live_server): STATIC_URL = '/static/' """ ) - def test_serve_static_with_staticfiles_app(self, django_testdir, settings): + def test_serve_static_with_staticfiles_app(self, django_testdir, settings) -> None: """ LiveServer always serves statics with ``django.contrib.staticfiles`` handler. @@ -453,7 +459,7 @@ def test_a(self, live_server, settings): result.stdout.fnmatch_lines(["*test_a*PASSED*"]) assert result.ret == 0 - def test_serve_static_dj17_without_staticfiles_app(self, live_server, settings): + def test_serve_static_dj17_without_staticfiles_app(self, live_server, settings) -> None: """ Because ``django.contrib.staticfiles`` is not installed LiveServer can not serve statics with django >= 1.7 . @@ -461,7 +467,7 @@ def test_serve_static_dj17_without_staticfiles_app(self, live_server, settings): with pytest.raises(HTTPError): urlopen(live_server + "/static/a_file.txt").read() - def test_specified_port_django_111(self, django_testdir): + def test_specified_port_django_111(self, django_testdir) -> None: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.bind(("", 0)) @@ -494,7 +500,7 @@ def test_with_live_server(live_server): ROOT_URLCONF = 'tpkg.app.urls' """ ) -def test_custom_user_model(django_testdir, username_field): +def test_custom_user_model(django_testdir, username_field) -> None: django_testdir.create_app_file( """ from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin @@ -614,7 +620,7 @@ class Migration(migrations.Migration): class Test_django_db_blocker: @pytest.mark.django_db - def test_block_manually(self, django_db_blocker): + def test_block_manually(self, django_db_blocker) -> None: try: django_db_blocker.block() with pytest.raises(RuntimeError): @@ -623,24 +629,24 @@ def test_block_manually(self, django_db_blocker): django_db_blocker.restore() @pytest.mark.django_db - def test_block_with_block(self, django_db_blocker): + def test_block_with_block(self, django_db_blocker) -> None: with django_db_blocker.block(): with pytest.raises(RuntimeError): Item.objects.exists() - def test_unblock_manually(self, django_db_blocker): + def test_unblock_manually(self, django_db_blocker) -> None: try: django_db_blocker.unblock() Item.objects.exists() finally: django_db_blocker.restore() - def test_unblock_with_block(self, django_db_blocker): + def test_unblock_with_block(self, django_db_blocker) -> None: with django_db_blocker.unblock(): Item.objects.exists() -def test_mail(mailoutbox): +def test_mail(mailoutbox) -> None: assert ( mailoutbox is mail.outbox ) # check that mail.outbox and fixture value is same object @@ -654,18 +660,18 @@ def test_mail(mailoutbox): assert list(m.to) == ["to@example.com"] -def test_mail_again(mailoutbox): +def test_mail_again(mailoutbox) -> None: test_mail(mailoutbox) -def test_mail_message_uses_mocked_DNS_NAME(mailoutbox): +def test_mail_message_uses_mocked_DNS_NAME(mailoutbox) -> None: mail.send_mail("subject", "body", "from@example.com", ["to@example.com"]) m = mailoutbox[0] message = m.message() assert message["Message-ID"].endswith("@fake-tests.example.com>") -def test_mail_message_uses_django_mail_dnsname_fixture(django_testdir): +def test_mail_message_uses_django_mail_dnsname_fixture(django_testdir) -> None: django_testdir.create_test_module( """ from django.core import mail @@ -688,7 +694,7 @@ def test_mailbox_inner(mailoutbox): assert result.ret == 0 -def test_mail_message_dns_patching_can_be_skipped(django_testdir): +def test_mail_message_dns_patching_can_be_skipped(django_testdir) -> None: django_testdir.create_test_module( """ from django.core import mail diff --git a/tests/test_initialization.py b/tests/test_initialization.py index b30c46f51..d8da80147 100644 --- a/tests/test_initialization.py +++ b/tests/test_initialization.py @@ -1,7 +1,7 @@ from textwrap import dedent -def test_django_setup_order_and_uniqueness(django_testdir, monkeypatch): +def test_django_setup_order_and_uniqueness(django_testdir, monkeypatch) -> None: """ The django.setup() function shall not be called multiple times by pytest-django, since it resets logging conf each time. diff --git a/tests/test_manage_py_scan.py b/tests/test_manage_py_scan.py index a11f87c24..071a4e0da 100644 --- a/tests/test_manage_py_scan.py +++ b/tests/test_manage_py_scan.py @@ -2,7 +2,7 @@ @pytest.mark.django_project(project_root="django_project_root", create_manage_py=True) -def test_django_project_found(django_testdir): +def test_django_project_found(django_testdir) -> None: # XXX: Important: Do not chdir() to django_project_root since runpytest_subprocess # will call "python /path/to/pytest.py", which will impliclity add cwd to # the path. By instead calling "python /path/to/pytest.py @@ -25,7 +25,7 @@ def test_foobar(): @pytest.mark.django_project(project_root="django_project_root", create_manage_py=True) -def test_django_project_found_with_k(django_testdir, monkeypatch): +def test_django_project_found_with_k(django_testdir, monkeypatch) -> None: """Test that cwd is checked as fallback with non-args via '-k foo'.""" testfile = django_testdir.create_test_module( """ @@ -44,7 +44,7 @@ def test_foobar(): @pytest.mark.django_project(project_root="django_project_root", create_manage_py=True) -def test_django_project_found_with_k_and_cwd(django_testdir, monkeypatch): +def test_django_project_found_with_k_and_cwd(django_testdir, monkeypatch) -> None: """Cover cwd not used as fallback if present already in args.""" testfile = django_testdir.create_test_module( """ @@ -63,7 +63,7 @@ def test_foobar(): @pytest.mark.django_project(project_root="django_project_root", create_manage_py=True) -def test_django_project_found_absolute(django_testdir, monkeypatch): +def test_django_project_found_absolute(django_testdir, monkeypatch) -> None: """This only tests that "." is added as an absolute path (#637).""" django_testdir.create_test_module( """ @@ -82,7 +82,7 @@ def test_dot_not_in_syspath(): @pytest.mark.django_project(project_root="django_project_root", create_manage_py=True) -def test_django_project_found_invalid_settings(django_testdir, monkeypatch): +def test_django_project_found_invalid_settings(django_testdir, monkeypatch) -> None: monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "DOES_NOT_EXIST") result = django_testdir.runpytest_subprocess("django_project_root") @@ -92,7 +92,7 @@ def test_django_project_found_invalid_settings(django_testdir, monkeypatch): result.stderr.fnmatch_lines(["*pytest-django found a Django project*"]) -def test_django_project_scan_disabled_invalid_settings(django_testdir, monkeypatch): +def test_django_project_scan_disabled_invalid_settings(django_testdir, monkeypatch) -> None: monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "DOES_NOT_EXIST") django_testdir.makeini( @@ -112,7 +112,7 @@ def test_django_project_scan_disabled_invalid_settings(django_testdir, monkeypat @pytest.mark.django_project(project_root="django_project_root", create_manage_py=True) -def test_django_project_found_invalid_settings_version(django_testdir, monkeypatch): +def test_django_project_found_invalid_settings_version(django_testdir, monkeypatch) -> None: """Invalid DSM should not cause an error with --help or --version.""" monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "DOES_NOT_EXIST") @@ -126,7 +126,7 @@ def test_django_project_found_invalid_settings_version(django_testdir, monkeypat @pytest.mark.django_project(project_root="django_project_root", create_manage_py=True) -def test_runs_without_error_on_long_args(django_testdir): +def test_runs_without_error_on_long_args(django_testdir) -> None: django_testdir.create_test_module( """ def test_this_is_a_long_message_which_caused_a_bug_when_scanning_for_manage_py_12346712341234123412341234123412341234123412341234123412341234123412341234123412341234123412341234123412341234123412341234123412341234123412341234123412341234112341234112451234123412341234123412341234123412341234123412341234123412341234123412341234123412341234(): diff --git a/tests/test_unittest.py b/tests/test_unittest.py index f9c01d9ed..665a5f1e4 100644 --- a/tests/test_unittest.py +++ b/tests/test_unittest.py @@ -7,32 +7,32 @@ class TestFixtures(TestCase): fixtures = ["items"] - def test_fixtures(self): + def test_fixtures(self) -> None: assert Item.objects.count() == 1 assert Item.objects.get().name == "Fixture item" - def test_fixtures_again(self): + def test_fixtures_again(self) -> None: """Ensure fixtures are only loaded once.""" self.test_fixtures() class TestSetup(TestCase): - def setUp(self): + def setUp(self) -> None: """setUp should be called after starting a transaction""" assert Item.objects.count() == 0 Item.objects.create(name="Some item") Item.objects.create(name="Some item again") - def test_count(self): + def test_count(self) -> None: self.assertEqual(Item.objects.count(), 2) assert Item.objects.count() == 2 Item.objects.create(name="Foo") self.assertEqual(Item.objects.count(), 3) - def test_count_again(self): + def test_count_again(self) -> None: self.test_count() - def tearDown(self): + def tearDown(self) -> None: """tearDown should be called before rolling back the database""" assert Item.objects.count() == 3 @@ -40,22 +40,22 @@ def tearDown(self): class TestFixturesWithSetup(TestCase): fixtures = ["items"] - def setUp(self): + def setUp(self) -> None: assert Item.objects.count() == 1 Item.objects.create(name="Some item") - def test_count(self): + def test_count(self) -> None: assert Item.objects.count() == 2 Item.objects.create(name="Some item again") - def test_count_again(self): + def test_count_again(self) -> None: self.test_count() - def tearDown(self): + def tearDown(self) -> None: assert Item.objects.count() == 3 -def test_sole_test(django_testdir): +def test_sole_test(django_testdir) -> None: """ Make sure the database is configured when only Django TestCase classes are collected, without the django_db marker. @@ -106,7 +106,7 @@ def test_bar(self): class TestUnittestMethods: "Test that setup/teardown methods of unittests are being called." - def test_django(self, django_testdir): + def test_django(self, django_testdir) -> None: django_testdir.create_test_module( """ from django.test import TestCase @@ -143,7 +143,7 @@ def test_pass(self): ) assert result.ret == 0 - def test_setUpClass_not_being_a_classmethod(self, django_testdir): + def test_setUpClass_not_being_a_classmethod(self, django_testdir) -> None: django_testdir.create_test_module( """ from django.test import TestCase @@ -165,7 +165,7 @@ def test_pass(self): result.stdout.fnmatch_lines(expected_lines) assert result.ret == 1 - def test_setUpClass_multiple_subclasses(self, django_testdir): + def test_setUpClass_multiple_subclasses(self, django_testdir) -> None: django_testdir.create_test_module( """ from django.test import TestCase @@ -203,7 +203,7 @@ def test_bar21(self): ) assert result.ret == 0 - def test_setUpClass_mixin(self, django_testdir): + def test_setUpClass_mixin(self, django_testdir) -> None: django_testdir.create_test_module( """ from django.test import TestCase @@ -231,7 +231,7 @@ def test_bar(self): ) assert result.ret == 0 - def test_setUpClass_skip(self, django_testdir): + def test_setUpClass_skip(self, django_testdir) -> None: django_testdir.create_test_module( """ from django.test import TestCase @@ -272,7 +272,7 @@ def test_bar21(self): ) assert result.ret == 0 - def test_multi_inheritance_setUpClass(self, django_testdir): + def test_multi_inheritance_setUpClass(self, django_testdir) -> None: django_testdir.create_test_module( """ from django.test import TestCase @@ -338,7 +338,7 @@ def test_c(self): assert result.parseoutcomes()["passed"] == 6 assert result.ret == 0 - def test_unittest(self, django_testdir): + def test_unittest(self, django_testdir) -> None: django_testdir.create_test_module( """ from unittest import TestCase @@ -375,7 +375,7 @@ def test_pass(self): ) assert result.ret == 0 - def test_setUpClass_leaf_but_not_in_dunder_dict(self, django_testdir): + def test_setUpClass_leaf_but_not_in_dunder_dict(self, django_testdir) -> None: django_testdir.create_test_module( """ from django.test import testcases @@ -407,7 +407,7 @@ def test_noop(self): class TestCaseWithDbFixture(TestCase): pytestmark = pytest.mark.usefixtures("db") - def test_simple(self): + def test_simple(self) -> None: # We only want to check setup/teardown does not conflict assert 1 @@ -415,12 +415,12 @@ def test_simple(self): class TestCaseWithTrDbFixture(TestCase): pytestmark = pytest.mark.usefixtures("transactional_db") - def test_simple(self): + def test_simple(self) -> None: # We only want to check setup/teardown does not conflict assert 1 -def test_pdb_enabled(django_testdir): +def test_pdb_enabled(django_testdir) -> None: """ Make sure the database is flushed and tests are isolated when using the --pdb option. @@ -465,7 +465,7 @@ def tearDown(self): assert result.ret == 0 -def test_debug_not_used(django_testdir): +def test_debug_not_used(django_testdir) -> None: django_testdir.create_test_module( """ from django.test import TestCase diff --git a/tests/test_urls.py b/tests/test_urls.py index 945540593..31cc0f6a2 100644 --- a/tests/test_urls.py +++ b/tests/test_urls.py @@ -5,18 +5,18 @@ @pytest.mark.urls("pytest_django_test.urls_overridden") -def test_urls(): +def test_urls() -> None: assert settings.ROOT_URLCONF == "pytest_django_test.urls_overridden" assert is_valid_path("/overridden_url/") @pytest.mark.urls("pytest_django_test.urls_overridden") -def test_urls_client(client): +def test_urls_client(client) -> None: response = client.get("/overridden_url/") assert force_str(response.content) == "Overridden urlconf works!" -def test_urls_cache_is_cleared(testdir): +def test_urls_cache_is_cleared(testdir) -> None: testdir.makepyfile( myurls=""" from django.urls import path @@ -49,7 +49,7 @@ def test_something_else(): assert result.ret == 0 -def test_urls_cache_is_cleared_and_new_urls_can_be_assigned(testdir): +def test_urls_cache_is_cleared_and_new_urls_can_be_assigned(testdir) -> None: testdir.makepyfile( myurls=""" from django.urls import path diff --git a/tests/test_without_django_loaded.py b/tests/test_without_django_loaded.py index eb6409947..1a7333daa 100644 --- a/tests/test_without_django_loaded.py +++ b/tests/test_without_django_loaded.py @@ -2,7 +2,7 @@ @pytest.fixture -def no_ds(monkeypatch): +def no_ds(monkeypatch) -> None: """Ensure DJANGO_SETTINGS_MODULE is unset""" monkeypatch.delenv("DJANGO_SETTINGS_MODULE") @@ -10,7 +10,7 @@ def no_ds(monkeypatch): pytestmark = pytest.mark.usefixtures("no_ds") -def test_no_ds(testdir): +def test_no_ds(testdir) -> None: testdir.makepyfile( """ import os @@ -26,7 +26,7 @@ def test_cfg(pytestconfig): assert r.ret == 0 -def test_database(testdir): +def test_database(testdir) -> None: testdir.makepyfile( """ import pytest @@ -51,7 +51,7 @@ def test_transactional_db(transactional_db): r.stdout.fnmatch_lines(["*4 skipped*"]) -def test_client(testdir): +def test_client(testdir) -> None: testdir.makepyfile( """ def test_client(client): @@ -66,7 +66,7 @@ def test_admin_client(admin_client): r.stdout.fnmatch_lines(["*2 skipped*"]) -def test_rf(testdir): +def test_rf(testdir) -> None: testdir.makepyfile( """ def test_rf(rf): @@ -78,7 +78,7 @@ def test_rf(rf): r.stdout.fnmatch_lines(["*1 skipped*"]) -def test_settings(testdir): +def test_settings(testdir) -> None: testdir.makepyfile( """ def test_settings(settings): @@ -90,7 +90,7 @@ def test_settings(settings): r.stdout.fnmatch_lines(["*1 skipped*"]) -def test_live_server(testdir): +def test_live_server(testdir) -> None: testdir.makepyfile( """ def test_live_server(live_server): @@ -102,7 +102,7 @@ def test_live_server(live_server): r.stdout.fnmatch_lines(["*1 skipped*"]) -def test_urls_mark(testdir): +def test_urls_mark(testdir) -> None: testdir.makepyfile( """ import pytest From af7ae0dc2baac96927645d046cdba909223469ec Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 8 May 2021 01:03:05 +0300 Subject: [PATCH 078/150] Add py.typed file to publish the types --- pytest_django/py.typed | 0 setup.cfg | 4 ++++ 2 files changed, 4 insertions(+) create mode 100644 pytest_django/py.typed diff --git a/pytest_django/py.typed b/pytest_django/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/setup.cfg b/setup.cfg index 7089af2d0..b25c69be2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,6 +38,7 @@ packages = pytest_django python_requires = >=3.5 setup_requires = setuptools_scm>=1.11.1 install_requires = pytest>=5.4.0 +zip_safe = no [options.entry_points] pytest11 = @@ -51,6 +52,9 @@ testing = Django django-configurations>=2.0 +[options.package_data] +pytest_django = py.typed + [tool:pytest] # --strict-markers: error on using unregistered marker. # -ra: show extra test summary info for everything. From 6f04c0c649fdbd351b38a39aacc9113526e473d6 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 8 May 2021 10:06:15 +0300 Subject: [PATCH 079/150] Add missing `reset_sequences` argument to `django_db` marker docstring --- pytest_django/plugin.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index e7e2211f9..12c2694d0 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -262,10 +262,12 @@ def pytest_load_initial_conftests( # Register the marks early_config.addinivalue_line( "markers", - "django_db(transaction=False): Mark the test as using " - "the Django test database. The *transaction* argument marks will " - "allow you to use real transactions in the test like Django's " - "TransactionTestCase.", + "django_db(transaction=False, reset_sequences=False): " + "Mark the test as using the Django test database. " + "The *transaction* argument allows you to use real transactions " + "in the test like Django's TransactionTestCase. ", + "The *reset_sequences* argument resets database sequences before " + "the test." ) early_config.addinivalue_line( "markers", From d42ab3b48e4de9d3861f6da9b7f779115fb52bcc Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 8 May 2021 11:03:29 +0300 Subject: [PATCH 080/150] Fix stray comma --- pytest_django/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 12c2694d0..4cd7f8cfd 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -265,9 +265,9 @@ def pytest_load_initial_conftests( "django_db(transaction=False, reset_sequences=False): " "Mark the test as using the Django test database. " "The *transaction* argument allows you to use real transactions " - "in the test like Django's TransactionTestCase. ", + "in the test like Django's TransactionTestCase. " "The *reset_sequences* argument resets database sequences before " - "the test." + "the test.", ) early_config.addinivalue_line( "markers", From 178b5e2c93c4c5964446a6ef3f61254a4e6b9dae Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 8 May 2021 11:17:33 +0300 Subject: [PATCH 081/150] Always use a TestCase subclass This is more flexible/orthogonal. --- pytest_django/fixtures.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 878c76bde..52473dc8f 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -145,25 +145,28 @@ def _django_db_fixture_helper( django_db_blocker.unblock() request.addfinalizer(django_db_blocker.restore) + import django.test + import django.db + if transactional: - from django.test import TransactionTestCase as django_case + test_case_class = django.test.TransactionTestCase + else: + test_case_class = django.test.TestCase - if reset_sequences: + _reset_sequences = reset_sequences - class ResetSequenceTestCase(django_case): - reset_sequences = True + class PytestDjangoTestCase(test_case_class): # type: ignore[misc,valid-type] + if transactional and _reset_sequences: + reset_sequences = True - django_case = ResetSequenceTestCase - else: - from django.test import TestCase as django_case - from django.db import transaction - transaction.Atomic._ensure_durability = False + if not transactional: + django.db.transaction.Atomic._ensure_durability = False def reset_durability() -> None: - transaction.Atomic._ensure_durability = True + django.db.transaction.Atomic._ensure_durability = True request.addfinalizer(reset_durability) - test_case = django_case(methodName="__init__") + test_case = PytestDjangoTestCase(methodName="__init__") test_case._pre_setup() request.addfinalizer(test_case._post_teardown) From 0e74f03f021546aabd838fa67d91910e70246e16 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 8 May 2021 11:48:23 +0300 Subject: [PATCH 082/150] Call Django's setUpClass()/tearDownClass() This lets Django do its thing without us having to implement it ourselves: - The durability stuff - Checking the `databases` attribute (not used yet) It does some other stuff that relies on attributes that we don't set so ends up a noop. --- pytest_django/fixtures.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 52473dc8f..f8dd47196 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -159,12 +159,8 @@ class PytestDjangoTestCase(test_case_class): # type: ignore[misc,valid-type] if transactional and _reset_sequences: reset_sequences = True - if not transactional: - django.db.transaction.Atomic._ensure_durability = False - - def reset_durability() -> None: - django.db.transaction.Atomic._ensure_durability = True - request.addfinalizer(reset_durability) + PytestDjangoTestCase.setUpClass() + request.addfinalizer(PytestDjangoTestCase.tearDownClass) test_case = PytestDjangoTestCase(methodName="__init__") test_case._pre_setup() From 29f71402f8d4bf99290a69ee25294c2b1a1296a8 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 8 May 2021 12:20:41 +0300 Subject: [PATCH 083/150] docs/faq: add missing add_arguments to `./manage.py test` recipe Fixes #837. --- docs/faq.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/faq.rst b/docs/faq.rst index 0249ebc78..a5a92ed6d 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -83,6 +83,13 @@ test runner like this: self.failfast = failfast self.keepdb = keepdb + @classmethod + def add_arguments(cls, parser): + parser.add_argument( + '--keepdb', action='store_true', + help='Preserves the test DB between runs.' + ) + def run_tests(self, test_labels): """Run pytest and return the exitcode. From 1ba6b3d47477f2ff3039bc7a1c5cafb05c0d2b82 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 8 May 2021 19:35:57 +0300 Subject: [PATCH 084/150] docs: replace odd page title It takes about more than just creation/re-use. --- docs/database.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/database.rst b/docs/database.rst index d23934a3d..d2740b266 100644 --- a/docs/database.rst +++ b/docs/database.rst @@ -1,5 +1,5 @@ -Database creation/re-use -======================== +Database access +=============== ``pytest-django`` takes a conservative approach to enabling database access. By default your tests will fail if they try to access the From 648e1b6d4857ecd3ee5b145adf81c344adf14a6b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 8 May 2021 20:18:25 +0300 Subject: [PATCH 085/150] tests: avoid if *all* connections support transactions Only interested in one connection. This causes some mess with mutli-db support in MySQL, which ends up querying all connections. --- tests/test_database.py | 15 +++++++-------- tests/test_fixtures.py | 3 +-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/tests/test_database.py b/tests/test_database.py index 7ad5f599b..c116e9967 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -1,6 +1,5 @@ import pytest from django.db import connection, transaction -from django.test.testcases import connections_support_transactions from pytest_django.lazy_django import get_django_version from pytest_django_test.app.models import Item @@ -66,13 +65,13 @@ def test_clean_db(self, all_dbs: None) -> None: assert Item.objects.count() == 0 def test_transactions_disabled(self, db: None) -> None: - if not connections_support_transactions(): + if not connection.features.supports_transactions: pytest.skip("transactions required for this test") assert connection.in_atomic_block def test_transactions_enabled(self, transactional_db: None) -> None: - if not connections_support_transactions(): + if not connection.features.supports_transactions: pytest.skip("transactions required for this test") assert not connection.in_atomic_block @@ -80,7 +79,7 @@ def test_transactions_enabled(self, transactional_db: None) -> None: def test_transactions_enabled_via_reset_seq( self, django_db_reset_sequences: None, ) -> None: - if not connections_support_transactions(): + if not connection.features.supports_transactions: pytest.skip("transactions required for this test") assert not connection.in_atomic_block @@ -120,7 +119,7 @@ def mydb(self, all_dbs: None) -> None: Item.objects.create(name="spam") def test_mydb(self, mydb: None) -> None: - if not connections_support_transactions(): + if not connection.features.supports_transactions: pytest.skip("transactions required for this test") # Check the fixture had access to the db @@ -196,21 +195,21 @@ def test_clean_db(self) -> None: @pytest.mark.django_db def test_transactions_disabled(self) -> None: - if not connections_support_transactions(): + if not connection.features.supports_transactions: pytest.skip("transactions required for this test") assert connection.in_atomic_block @pytest.mark.django_db(transaction=False) def test_transactions_disabled_explicit(self) -> None: - if not connections_support_transactions(): + if not connection.features.supports_transactions: pytest.skip("transactions required for this test") assert connection.in_atomic_block @pytest.mark.django_db(transaction=True) def test_transactions_enabled(self) -> None: - if not connections_support_transactions(): + if not connection.features.supports_transactions: pytest.skip("transactions required for this test") assert not connection.in_atomic_block diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index d8e03adc5..fe3cb94ba 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -14,7 +14,6 @@ from django.core import mail from django.db import connection, transaction from django.test.client import Client, RequestFactory -from django.test.testcases import connections_support_transactions from django.utils.encoding import force_str from pytest_django_test.app.models import Item @@ -374,7 +373,7 @@ def test_settings_restored(self) -> None: assert settings.ALLOWED_HOSTS == ["testserver"] def test_transactions(self, live_server) -> None: - if not connections_support_transactions(): + if not connection.features.support_transactions: pytest.skip("transactions required for this test") assert not connection.in_atomic_block From d3f3d4445c5078064b504fec263e5f11e860dfb1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 8 May 2021 20:25:49 +0300 Subject: [PATCH 086/150] tests: fix a typo in the previous commit --- tests/test_fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index fe3cb94ba..c0ba3a2ce 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -373,7 +373,7 @@ def test_settings_restored(self) -> None: assert settings.ALLOWED_HOSTS == ["testserver"] def test_transactions(self, live_server) -> None: - if not connection.features.support_transactions: + if not connection.features.supports_transactions: pytest.skip("transactions required for this test") assert not connection.in_atomic_block From 59d0bf3c3ca691c58e8dc1f33e27846f20e8ed6f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 8 May 2021 20:39:55 +0300 Subject: [PATCH 087/150] coverage: exclude TYPE_CHECKING blocks from coverage --- .coveragerc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.coveragerc b/.coveragerc index 168f57853..4198d9593 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,3 +6,6 @@ branch = 1 [report] include = pytest_django/*,pytest_django_test/*,tests/* skip_covered = 1 +exclude_lines = + pragma: no cover + if TYPE_CHECKING: From 92c6b7ef6ee2bf475e529dbc3dcbbd7cbd61e593 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 8 May 2021 10:00:26 +0300 Subject: [PATCH 088/150] Add initial experimental multi database support --- docs/database.rst | 26 ++++++++------ docs/helpers.rst | 19 ++++++++++- pytest_django/fixtures.py | 13 ++++++- pytest_django/plugin.py | 27 ++++++++++----- .../app/migrations/0001_initial.py | 17 +++++++++- pytest_django_test/app/models.py | 6 ++++ pytest_django_test/db_helpers.py | 3 ++ pytest_django_test/db_router.py | 14 ++++++++ pytest_django_test/settings_base.py | 2 ++ pytest_django_test/settings_mysql_innodb.py | 33 +++++++++++++++++- pytest_django_test/settings_mysql_myisam.py | 33 +++++++++++++++++- pytest_django_test/settings_postgres.py | 19 ++++++++++- pytest_django_test/settings_sqlite.py | 14 +++++++- pytest_django_test/settings_sqlite_file.py | 23 ++++++++++--- tests/conftest.py | 7 +++- tests/test_database.py | 34 ++++++++++++++++++- tests/test_db_setup.py | 9 +++++ 17 files changed, 266 insertions(+), 33 deletions(-) create mode 100644 pytest_django_test/db_router.py diff --git a/docs/database.rst b/docs/database.rst index d2740b266..004342b4f 100644 --- a/docs/database.rst +++ b/docs/database.rst @@ -64,21 +64,25 @@ select using an argument to the ``django_db`` mark:: Tests requiring multiple databases ---------------------------------- +.. caution:: + + This support is **experimental** and is subject to change without + deprecation. We are still figuring out the best way to expose this + functionality. If you are using this successfully or unsuccessfully, + `let us know `_! + +``pytest-django`` has experimental support for multi-database configurations. Currently ``pytest-django`` does not specifically support Django's -multi-database support. +multi-database support, using the ``databases`` argument to the +:py:func:`django_db ` mark:: -You can however use normal :class:`~django.test.TestCase` instances to use its -:ref:`django:topics-testing-advanced-multidb` support. -In particular, if your database is configured for replication, be sure to read -about :ref:`django:topics-testing-primaryreplica`. + @pytest.mark.django_db(databases=['default', 'other']) + def test_spam(): + assert MyModel.objects.using('other').count() == 0 -If you have any ideas about the best API to support multiple databases -directly in ``pytest-django`` please get in touch, we are interested -in eventually supporting this but unsure about simply following -Django's approach. +For details see :py:attr:`django.test.TransactionTestCase.databases` and +:py:attr:`django.test.TestCase.databases`. -See `pull request 431 `_ -for an idea/discussion to approach this. ``--reuse-db`` - reuse the testing database between test runs -------------------------------------------------------------- diff --git a/docs/helpers.rst b/docs/helpers.rst index d035f7a61..07fe43a6b 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -24,7 +24,7 @@ Markers ``pytest.mark.django_db`` - request database access ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. py:function:: pytest.mark.django_db([transaction=False, reset_sequences=False]) +.. py:function:: pytest.mark.django_db([transaction=False, reset_sequences=False, databases=None]) This is used to mark a test function as requiring the database. It will ensure the database is set up correctly for the test. Each test @@ -54,6 +54,23 @@ Markers effect. Please be aware that not all databases support this feature. For details see :py:attr:`django.test.TransactionTestCase.reset_sequences`. + + :type databases: Union[Iterable[str], str, None] + :param databases: + .. caution:: + + This argument is **experimental** and is subject to change without + deprecation. We are still figuring out the best way to expose this + functionality. If you are using this successfully or unsuccessfully, + `let us know `_! + + The ``databases`` argument defines which databases in a multi-database + configuration will be set up and may be used by the test. Defaults to + only the ``default`` database. The special value ``"__all__"`` may be use + to specify all configured databases. + For details see :py:attr:`django.test.TransactionTestCase.databases` and + :py:attr:`django.test.TestCase.databases`. + .. note:: If you want access to the Django database inside a *fixture*, this marker may diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index f8dd47196..b462ad933 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -1,5 +1,5 @@ """All pytest-django fixtures""" -from typing import Any, Generator, List +from typing import Any, Generator, Iterable, List, Optional, Tuple, Union import os from contextlib import contextmanager from functools import partial @@ -12,8 +12,13 @@ TYPE_CHECKING = False if TYPE_CHECKING: + from typing import Literal + import django + _DjangoDbDatabases = Optional[Union["Literal['__all__']", Iterable[str]]] + _DjangoDb = Tuple[bool, bool, _DjangoDbDatabases] + __all__ = [ "django_db_setup", @@ -142,6 +147,10 @@ def _django_db_fixture_helper( # Do nothing, we get called with transactional=True, too. return + _databases = getattr( + request.node, "_pytest_django_databases", None, + ) # type: Optional[_DjangoDbDatabases] + django_db_blocker.unblock() request.addfinalizer(django_db_blocker.restore) @@ -158,6 +167,8 @@ def _django_db_fixture_helper( class PytestDjangoTestCase(test_case_class): # type: ignore[misc,valid-type] if transactional and _reset_sequences: reset_sequences = True + if _databases is not None: + databases = _databases PytestDjangoTestCase.setUpClass() request.addfinalizer(PytestDjangoTestCase.tearDownClass) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 4cd7f8cfd..de1d9c2ea 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -48,6 +48,8 @@ import django + from .fixtures import _DjangoDb, _DjangoDbDatabases + SETTINGS_MODULE_ENV = "DJANGO_SETTINGS_MODULE" CONFIGURATION_ENV = "DJANGO_CONFIGURATION" @@ -262,12 +264,14 @@ def pytest_load_initial_conftests( # Register the marks early_config.addinivalue_line( "markers", - "django_db(transaction=False, reset_sequences=False): " + "django_db(transaction=False, reset_sequences=False, databases=None): " "Mark the test as using the Django test database. " "The *transaction* argument allows you to use real transactions " "in the test like Django's TransactionTestCase. " "The *reset_sequences* argument resets database sequences before " - "the test.", + "the test. " + "The *databases* argument sets which database aliases the test " + "uses (by default, only 'default'). Use '__all__' for all databases.", ) early_config.addinivalue_line( "markers", @@ -452,7 +456,11 @@ def _django_db_marker(request) -> None: """ marker = request.node.get_closest_marker("django_db") if marker: - transaction, reset_sequences = validate_django_db(marker) + transaction, reset_sequences, databases = validate_django_db(marker) + + # TODO: Use pytest Store (item.store) once that's stable. + request.node._pytest_django_databases = databases + if reset_sequences: request.getfixturevalue("django_db_reset_sequences") elif transaction: @@ -727,12 +735,12 @@ def restore(self) -> None: _blocking_manager = _DatabaseBlocker() -def validate_django_db(marker) -> Tuple[bool, bool]: +def validate_django_db(marker) -> "_DjangoDb": """Validate the django_db marker. - It checks the signature and creates the ``transaction`` and - ``reset_sequences`` attributes on the marker which will have the - correct values. + It checks the signature and creates the ``transaction``, + ``reset_sequences`` and ``databases`` attributes on the marker + which will have the correct values. A sequence reset is only allowed when combined with a transaction. """ @@ -740,8 +748,9 @@ def validate_django_db(marker) -> Tuple[bool, bool]: def apifun( transaction: bool = False, reset_sequences: bool = False, - ) -> Tuple[bool, bool]: - return transaction, reset_sequences + databases: "_DjangoDbDatabases" = None, + ) -> "_DjangoDb": + return transaction, reset_sequences, databases return apifun(*marker.args, **marker.kwargs) diff --git a/pytest_django_test/app/migrations/0001_initial.py b/pytest_django_test/app/migrations/0001_initial.py index 5b0415afc..8953f3be6 100644 --- a/pytest_django_test/app/migrations/0001_initial.py +++ b/pytest_django_test/app/migrations/0001_initial.py @@ -24,5 +24,20 @@ class Migration(migrations.Migration): ), ("name", models.CharField(max_length=100)), ], - ) + ), + migrations.CreateModel( + name="SecondItem", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ], + ), ] diff --git a/pytest_django_test/app/models.py b/pytest_django_test/app/models.py index 804d36020..5186adc41 100644 --- a/pytest_django_test/app/models.py +++ b/pytest_django_test/app/models.py @@ -1,5 +1,11 @@ from django.db import models +# Routed to database "main". class Item(models.Model): name = models.CharField(max_length=100) # type: str + + +# Routed to database "second". +class SecondItem(models.Model): + name = models.CharField(max_length=100) # type: str diff --git a/pytest_django_test/db_helpers.py b/pytest_django_test/db_helpers.py index d3ec63764..a451ba86a 100644 --- a/pytest_django_test/db_helpers.py +++ b/pytest_django_test/db_helpers.py @@ -26,6 +26,9 @@ # An explicit test db name was given, is that as the base name TEST_DB_NAME = "{}_inner".format(TEST_DB_NAME) +SECOND_DB_NAME = DB_NAME + '_second' if DB_NAME is not None else None +SECOND_TEST_DB_NAME = TEST_DB_NAME + '_second' if DB_NAME is not None else None + def get_db_engine(): return _settings["ENGINE"].split(".")[-1] diff --git a/pytest_django_test/db_router.py b/pytest_django_test/db_router.py new file mode 100644 index 000000000..c2486e957 --- /dev/null +++ b/pytest_django_test/db_router.py @@ -0,0 +1,14 @@ +class DbRouter: + def db_for_read(self, model, **hints): + if model._meta.app_label == 'app' and model._meta.model_name == 'seconditem': + return 'second' + return None + + def db_for_write(self, model, **hints): + if model._meta.app_label == 'app' and model._meta.model_name == 'seconditem': + return 'second' + return None + + def allow_migrate(self, db, app_label, model_name=None, **hints): + if app_label == 'app' and model_name == 'seconditem': + return db == 'second' diff --git a/pytest_django_test/settings_base.py b/pytest_django_test/settings_base.py index 4c9b456f9..ff8dc2d39 100644 --- a/pytest_django_test/settings_base.py +++ b/pytest_django_test/settings_base.py @@ -27,3 +27,5 @@ "OPTIONS": {}, } ] + +DATABASE_ROUTERS = ['pytest_django_test.db_router.DbRouter'] diff --git a/pytest_django_test/settings_mysql_innodb.py b/pytest_django_test/settings_mysql_innodb.py index a3163b096..062cfac03 100644 --- a/pytest_django_test/settings_mysql_innodb.py +++ b/pytest_django_test/settings_mysql_innodb.py @@ -6,7 +6,38 @@ DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", - "NAME": "pytest_django_should_never_get_accessed", + "NAME": "pytest_django_tests_default", + "USER": environ.get("TEST_DB_USER", "root"), + "PASSWORD": environ.get("TEST_DB_PASSWORD", ""), + "HOST": environ.get("TEST_DB_HOST", "localhost"), + "OPTIONS": { + "init_command": "SET default_storage_engine=InnoDB", + "charset": "utf8mb4", + }, + "TEST": { + "CHARSET": "utf8mb4", + "COLLATION": "utf8mb4_unicode_ci", + }, + }, + "replica": { + "ENGINE": "django.db.backends.mysql", + "NAME": "pytest_django_tests_replica", + "USER": environ.get("TEST_DB_USER", "root"), + "PASSWORD": environ.get("TEST_DB_PASSWORD", ""), + "HOST": environ.get("TEST_DB_HOST", "localhost"), + "OPTIONS": { + "init_command": "SET default_storage_engine=InnoDB", + "charset": "utf8mb4", + }, + "TEST": { + "MIRROR": "default", + "CHARSET": "utf8mb4", + "COLLATION": "utf8mb4_unicode_ci", + }, + }, + "second": { + "ENGINE": "django.db.backends.mysql", + "NAME": "pytest_django_tests_second", "USER": environ.get("TEST_DB_USER", "root"), "PASSWORD": environ.get("TEST_DB_PASSWORD", ""), "HOST": environ.get("TEST_DB_HOST", "localhost"), diff --git a/pytest_django_test/settings_mysql_myisam.py b/pytest_django_test/settings_mysql_myisam.py index c4f9fc592..d939b7cb9 100644 --- a/pytest_django_test/settings_mysql_myisam.py +++ b/pytest_django_test/settings_mysql_myisam.py @@ -6,7 +6,38 @@ DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", - "NAME": "pytest_django_should_never_get_accessed", + "NAME": "pytest_django_tests_default", + "USER": environ.get("TEST_DB_USER", "root"), + "PASSWORD": environ.get("TEST_DB_PASSWORD", ""), + "HOST": environ.get("TEST_DB_HOST", "localhost"), + "OPTIONS": { + "init_command": "SET default_storage_engine=MyISAM", + "charset": "utf8mb4", + }, + "TEST": { + "CHARSET": "utf8mb4", + "COLLATION": "utf8mb4_unicode_ci", + }, + }, + "replica": { + "ENGINE": "django.db.backends.mysql", + "NAME": "pytest_django_tests_replica", + "USER": environ.get("TEST_DB_USER", "root"), + "PASSWORD": environ.get("TEST_DB_PASSWORD", ""), + "HOST": environ.get("TEST_DB_HOST", "localhost"), + "OPTIONS": { + "init_command": "SET default_storage_engine=MyISAM", + "charset": "utf8mb4", + }, + "TEST": { + "MIRROR": "default", + "CHARSET": "utf8mb4", + "COLLATION": "utf8mb4_unicode_ci", + }, + }, + "second": { + "ENGINE": "django.db.backends.mysql", + "NAME": "pytest_django_tests_second", "USER": environ.get("TEST_DB_USER", "root"), "PASSWORD": environ.get("TEST_DB_PASSWORD", ""), "HOST": environ.get("TEST_DB_HOST", "localhost"), diff --git a/pytest_django_test/settings_postgres.py b/pytest_django_test/settings_postgres.py index 5c387ef7b..af742433e 100644 --- a/pytest_django_test/settings_postgres.py +++ b/pytest_django_test/settings_postgres.py @@ -14,7 +14,24 @@ DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", - "NAME": "pytest_django_should_never_get_accessed", + "NAME": "pytest_django_tests_default", + "USER": environ.get("TEST_DB_USER", ""), + "PASSWORD": environ.get("TEST_DB_PASSWORD", ""), + "HOST": environ.get("TEST_DB_HOST", ""), + }, + "replica": { + "ENGINE": "django.db.backends.postgresql", + "NAME": "pytest_django_tests_replica", + "USER": environ.get("TEST_DB_USER", ""), + "PASSWORD": environ.get("TEST_DB_PASSWORD", ""), + "HOST": environ.get("TEST_DB_HOST", ""), + "TEST": { + "MIRROR": "default", + }, + }, + "second": { + "ENGINE": "django.db.backends.postgresql", + "NAME": "pytest_django_tests_second", "USER": environ.get("TEST_DB_USER", ""), "PASSWORD": environ.get("TEST_DB_PASSWORD", ""), "HOST": environ.get("TEST_DB_HOST", ""), diff --git a/pytest_django_test/settings_sqlite.py b/pytest_django_test/settings_sqlite.py index 8ace0293b..f58cc7d89 100644 --- a/pytest_django_test/settings_sqlite.py +++ b/pytest_django_test/settings_sqlite.py @@ -1,8 +1,20 @@ from .settings_base import * # noqa: F401 F403 + DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": "/should_not_be_accessed", - } + }, + "replica": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": "/should_not_be_accessed", + "TEST": { + "MIRROR": "default", + }, + }, + "second": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": "/should_not_be_accessed", + }, } diff --git a/pytest_django_test/settings_sqlite_file.py b/pytest_django_test/settings_sqlite_file.py index a4e77ab11..02395561a 100644 --- a/pytest_django_test/settings_sqlite_file.py +++ b/pytest_django_test/settings_sqlite_file.py @@ -6,12 +6,27 @@ # tests (via setting TEST_NAME / TEST['NAME']). # The name as expected / used by Django/pytest_django (tests/db_helpers.py). -_fd, _filename = tempfile.mkstemp(prefix="test_") +_fd, _filename_default = tempfile.mkstemp(prefix="test_") +_fd, _filename_replica = tempfile.mkstemp(prefix="test_") +_fd, _filename_second = tempfile.mkstemp(prefix="test_") DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", - "NAME": "/pytest_django_should_never_get_accessed", - "TEST": {"NAME": _filename}, - } + "NAME": "/pytest_django_tests_default", + "TEST": {"NAME": _filename_default}, + }, + "replica": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": "/pytest_django_tests_replica", + "TEST": { + "MIRROR": "default", + "NAME": _filename_replica, + }, + }, + "second": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": "/pytest_django_tests_second", + "TEST": {"NAME": _filename_second}, + }, } diff --git a/tests/conftest.py b/tests/conftest.py index 1e7878a5e..414f3035f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,7 +39,9 @@ def testdir(testdir, monkeypatch): @pytest.fixture(scope="function") def django_testdir(request, testdir, monkeypatch): - from pytest_django_test.db_helpers import DB_NAME, TEST_DB_NAME + from pytest_django_test.db_helpers import ( + DB_NAME, TEST_DB_NAME, SECOND_DB_NAME, SECOND_TEST_DB_NAME, + ) marker = request.node.get_closest_marker("django_project") @@ -51,6 +53,8 @@ def django_testdir(request, testdir, monkeypatch): db_settings = copy.deepcopy(settings.DATABASES) db_settings["default"]["NAME"] = DB_NAME db_settings["default"]["TEST"]["NAME"] = TEST_DB_NAME + db_settings["second"]["NAME"] = SECOND_DB_NAME + db_settings["second"].setdefault("TEST", {})["NAME"] = SECOND_TEST_DB_NAME test_settings = ( dedent( @@ -66,6 +70,7 @@ def django_testdir(request, testdir, monkeypatch): compat.register() DATABASES = %(db_settings)s + DATABASE_ROUTERS = ['pytest_django_test.db_router.DbRouter'] INSTALLED_APPS = [ 'django.contrib.auth', diff --git a/tests/test_database.py b/tests/test_database.py index c116e9967..9b5a88bdc 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -2,7 +2,7 @@ from django.db import connection, transaction from pytest_django.lazy_django import get_django_version -from pytest_django_test.app.models import Item +from pytest_django_test.app.models import Item, SecondItem def db_supports_reset_sequences() -> bool: @@ -224,6 +224,38 @@ def test_reset_sequences_enabled(self, request) -> None: marker = request.node.get_closest_marker("django_db") assert marker.kwargs["reset_sequences"] + @pytest.mark.django_db(databases=['default', 'replica', 'second']) + def test_databases(self, request) -> None: + marker = request.node.get_closest_marker("django_db") + assert marker.kwargs["databases"] == ['default', 'replica', 'second'] + + @pytest.mark.django_db(databases=['second']) + def test_second_database(self, request) -> None: + SecondItem.objects.create(name="spam") + + @pytest.mark.django_db(databases=['default']) + def test_not_allowed_database(self, request) -> None: + with pytest.raises(AssertionError, match='not allowed'): + SecondItem.objects.count() + with pytest.raises(AssertionError, match='not allowed'): + SecondItem.objects.create(name="spam") + + @pytest.mark.django_db(databases=['replica']) + def test_replica_database(self, request) -> None: + Item.objects.using('replica').count() + + @pytest.mark.django_db(databases=['replica']) + def test_replica_database_not_allowed(self, request) -> None: + with pytest.raises(AssertionError, match='not allowed'): + Item.objects.count() + + @pytest.mark.django_db(databases='__all__') + def test_all_databases(self, request) -> None: + Item.objects.count() + Item.objects.create(name="spam") + SecondItem.objects.count() + SecondItem.objects.create(name="spam") + def test_unittest_interaction(django_testdir) -> None: "Test that (non-Django) unittests cannot access the DB." diff --git a/tests/test_db_setup.py b/tests/test_db_setup.py index d8d339c01..c22d9a8dd 100644 --- a/tests/test_db_setup.py +++ b/tests/test_db_setup.py @@ -519,6 +519,15 @@ class Migration(migrations.Migration): }, bases=(models.Model,), ), + migrations.CreateModel( + name='SecondItem', + fields=[ + ('id', models.AutoField(serialize=False, + auto_created=True, + primary_key=True)), + ('name', models.CharField(max_length=100)), + ], + ), migrations.RunPython( print_it, ), From 6ab338f54d1452b012e7bc4b1fee31b168efa97e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 14 May 2021 11:59:49 +0300 Subject: [PATCH 089/150] doc/helpers: more directly mention that marks can be scoped --- docs/helpers.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/helpers.rst b/docs/helpers.rst index 07fe43a6b..b88eb64b7 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -18,7 +18,9 @@ Markers ``pytest-django`` registers and uses markers. See the pytest :ref:`documentation ` on what marks are and for notes on -:ref:`using ` them. +:ref:`using ` them. Remember that you can apply +marks at the single test level, the class level, the module level, and +dynamically in a hook or fixture. ``pytest.mark.django_db`` - request database access From a54ae984741ae22313deac1c95bae40ce2bf6907 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 15 May 2021 20:21:53 +0300 Subject: [PATCH 090/150] asserts: add type annotations It's the only user-visible part so let's put some effort. --- pytest_django/asserts.py | 169 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 168 insertions(+), 1 deletion(-) diff --git a/pytest_django/asserts.py b/pytest_django/asserts.py index 2da2f5fbd..a0ac40c12 100644 --- a/pytest_django/asserts.py +++ b/pytest_django/asserts.py @@ -1,7 +1,7 @@ """ Dynamically load all Django assertion cases and expose them for importing. """ -from typing import Any, Callable, Set +from typing import Any, Callable, Optional, Sequence, Set, Union from functools import wraps from django.test import ( @@ -40,5 +40,172 @@ def assertion_func(*args, **kwargs): if TYPE_CHECKING: + from django.http import HttpResponse + + def assertRedirects( + response: HttpResponse, + expected_url: str, + status_code: int = ..., + target_status_code: int = ..., + msg_prefix: str = ..., + fetch_redirect_response: bool = ..., + ) -> None: + ... + + def assertURLEqual( + url1: str, + url2: str, + msg_prefix: str = ..., + ) -> None: + ... + + def assertContains( + response: HttpResponse, + text: object, + count: Optional[int] = ..., + status_code: int = ..., + msg_prefix: str = ..., + html: bool = False, + ) -> None: + ... + + def assertNotContains( + response: HttpResponse, + text: object, + status_code: int = ..., + msg_prefix: str = ..., + html: bool = False, + ) -> None: + ... + + def assertFormError( + response: HttpResponse, + form: str, + field: Optional[str], + errors: Union[str, Sequence[str]], + msg_prefix: str = ..., + ) -> None: + ... + + def assertFormsetError( + response: HttpResponse, + formset: str, + form_index: Optional[int], + field: Optional[str], + errors: Union[str, Sequence[str]], + msg_prefix: str = ..., + ) -> None: + ... + + def assertTemplateUsed( + response: Optional[HttpResponse] = ..., + template_name: Optional[str] = ..., + msg_prefix: str = ..., + count: Optional[int] = ..., + ) -> None: + ... + + def assertTemplateNotUsed( + response: Optional[HttpResponse] = ..., + template_name: Optional[str] = ..., + msg_prefix: str = ..., + ) -> None: + ... + + def assertRaisesMessage( + expected_exception: BaseException, + expected_message: str, + *args, + **kwargs, + ): + ... + + def assertWarnsMessage( + expected_warning: Warning, + expected_message: str, + *args, + **kwargs, + ): + ... + + def assertFieldOutput( + fieldclass, + valid, + invalid, + field_args=..., + field_kwargs=..., + empty_value: str = ..., + ) -> None: + ... + + def assertHTMLEqual( + html1: str, + html2: str, + msg: Optional[str] = ..., + ) -> None: + ... + + def assertHTMLNotEqual( + html1: str, + html2: str, + msg: Optional[str] = ..., + ) -> None: + ... + + def assertInHTML( + needle: str, + haystack: str, + count: Optional[int] = ..., + msg_prefix: str = ..., + ) -> None: + ... + + def assertJSONEqual( + raw: str, + expected_data: Any, + msg: Optional[str] = ..., + ) -> None: + ... + + def assertJSONNotEqual( + raw: str, + expected_data: Any, + msg: Optional[str] = ..., + ) -> None: + ... + + def assertXMLEqual( + xml1: str, + xml2: str, + msg: Optional[str] = ..., + ) -> None: + ... + + def assertXMLNotEqual( + xml1: str, + xml2: str, + msg: Optional[str] = ..., + ) -> None: + ... + + def assertQuerysetEqual( + qs, + values, + transform=..., + ordered: bool = ..., + msg: Optional[str] = ..., + ) -> None: + ... + + def assertNumQueries( + num: int, + func=..., + *args, + using: str = ..., + **kwargs, + ): + ... + + # Fallback in case Django adds new asserts. def __getattr__(name: str) -> Callable[..., Any]: ... From 7924519c309b1df9719cd539bc665228f7e5b376 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 15 May 2021 20:58:57 +0300 Subject: [PATCH 091/150] asserts: fix annoying py35 syntax error --- pytest_django/asserts.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pytest_django/asserts.py b/pytest_django/asserts.py index a0ac40c12..ab8a5dc53 100644 --- a/pytest_django/asserts.py +++ b/pytest_django/asserts.py @@ -116,7 +116,7 @@ def assertRaisesMessage( expected_exception: BaseException, expected_message: str, *args, - **kwargs, + **kwargs ): ... @@ -124,7 +124,7 @@ def assertWarnsMessage( expected_warning: Warning, expected_message: str, *args, - **kwargs, + **kwargs ): ... @@ -202,7 +202,7 @@ def assertNumQueries( func=..., *args, using: str = ..., - **kwargs, + **kwargs ): ... From 44fddc2082db16e96e77d388db0ef94812f06226 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 15 May 2021 20:51:46 +0300 Subject: [PATCH 092/150] Release 4.3.0 --- docs/changelog.rst | 14 ++++++++++++++ docs/database.rst | 3 +++ 2 files changed, 17 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9d59a7e9e..9f3e438cc 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,20 @@ Changelog ========= +v4.3.0 (2021-05-15) +------------------- + +Improvements +^^^^^^^^^^^^ + +* Add experimental :ref:`multiple databases ` (multi db) support. + +* Add type annotations. If you previously excluded ``pytest_django`` from + your type-checker, you can remove the exclusion. + +* Documentation improvements. + + v4.2.0 (2021-04-10) ------------------- diff --git a/docs/database.rst b/docs/database.rst index 004342b4f..2f3115991 100644 --- a/docs/database.rst +++ b/docs/database.rst @@ -60,10 +60,13 @@ select using an argument to the ``django_db`` mark:: def test_spam(): pass # test relying on transactions +.. _`multi-db`: Tests requiring multiple databases ---------------------------------- +.. versionadded:: 4.3 + .. caution:: This support is **experimental** and is subject to change without From b90fea560ab2dfc00b7ac8996674e371f0103a3f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 24 May 2021 15:14:35 +0300 Subject: [PATCH 093/150] fixtures: add django_capture_on_commit_callbacks fixture Similar to Django's `TestCase.captureOnCommitCallbacks`. Documentation is cribbed from there. Fixes #752. --- docs/helpers.rst | 45 +++++++++++++++++++++++++++ pytest_django/fixtures.py | 40 ++++++++++++++++++++++-- pytest_django/plugin.py | 1 + tests/test_fixtures.py | 64 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 148 insertions(+), 2 deletions(-) diff --git a/docs/helpers.rst b/docs/helpers.rst index b88eb64b7..774237b39 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -425,6 +425,51 @@ Example usage:: Item.objects.create('foo') Item.objects.create('bar') + +.. fixture:: django_capture_on_commit_callbacks + +``django_capture_on_commit_callbacks`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. py:function:: django_capture_on_commit_callbacks(*, using=DEFAULT_DB_ALIAS, execute=False) + + :param using: + The alias of the database connection to capture callbacks for. + :param execute: + If True, all the callbacks will be called as the context manager exits, if + no exception occurred. This emulates a commit after the wrapped block of + code. + +.. versionadded:: 4.4 + +Returns a context manager that captures +:func:`transaction.on_commit() ` callbacks for +the given database connection. It returns a list that contains, on exit of the +context, the captured callback functions. From this list you can make assertions +on the callbacks or call them to invoke their side effects, emulating a commit. + +Avoid this fixture in tests using ``transaction=True``; you are not likely to +get useful results. + +This fixture is based on Django's :meth:`django.test.TestCase.captureOnCommitCallbacks` +helper. + +Example usage:: + + def test_on_commit(client, mailoutbox, django_capture_on_commit_callbacks): + with django_capture_on_commit_callbacks(execute=True) as callbacks: + response = client.post( + '/contact/', + {'message': 'I like your site'}, + ) + + assert response.status_code == 200 + assert len(callbacks) == 1 + assert len(mailoutbox) == 1 + assert mailoutbox[0].subject == 'Contact Form' + assert mailoutbox[0].body == 'I like your site' + + .. fixture:: mailoutbox ``mailoutbox`` diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index b462ad933..b58aadeba 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -1,5 +1,5 @@ """All pytest-django fixtures""" -from typing import Any, Generator, Iterable, List, Optional, Tuple, Union +from typing import Any, Callable, Generator, Iterable, List, Optional, Tuple, Union import os from contextlib import contextmanager from functools import partial @@ -8,7 +8,7 @@ from . import live_server_helper from .django_compat import is_django_unittest -from .lazy_django import skip_if_no_django +from .lazy_django import skip_if_no_django, get_django_version TYPE_CHECKING = False if TYPE_CHECKING: @@ -38,6 +38,7 @@ "_live_server_helper", "django_assert_num_queries", "django_assert_max_num_queries", + "django_capture_on_commit_callbacks", ] @@ -542,3 +543,38 @@ def django_assert_num_queries(pytestconfig): @pytest.fixture(scope="function") def django_assert_max_num_queries(pytestconfig): return partial(_assert_num_queries, pytestconfig, exact=False) + + +@contextmanager +def _capture_on_commit_callbacks( + *, + using: Optional[str] = None, + execute: bool = False +): + from django.db import DEFAULT_DB_ALIAS, connections + from django.test import TestCase + + if using is None: + using = DEFAULT_DB_ALIAS + + # Polyfill of Django code as of Django 3.2. + if get_django_version() < (3, 2): + callbacks = [] # type: List[Callable[[], Any]] + start_count = len(connections[using].run_on_commit) + try: + yield callbacks + finally: + run_on_commit = connections[using].run_on_commit[start_count:] + callbacks[:] = [func for sids, func in run_on_commit] + if execute: + for callback in callbacks: + callback() + + else: + with TestCase.captureOnCommitCallbacks(using=using, execute=execute) as callbacks: + yield callbacks + + +@pytest.fixture(scope="function") +def django_capture_on_commit_callbacks(): + return _capture_on_commit_callbacks diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index de1d9c2ea..3e9dd9c68 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -25,6 +25,7 @@ from .fixtures import django_db_modify_db_settings_parallel_suffix # noqa from .fixtures import django_db_modify_db_settings_tox_suffix # noqa from .fixtures import django_db_modify_db_settings_xdist_suffix # noqa +from .fixtures import django_capture_on_commit_callbacks # noqa from .fixtures import _live_server_helper # noqa from .fixtures import admin_client # noqa from .fixtures import admin_user # noqa diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index c0ba3a2ce..a833d32bd 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -230,6 +230,70 @@ def test_queries(django_assert_num_queries): assert result.ret == 1 +@pytest.mark.django_db +def test_django_capture_on_commit_callbacks(django_capture_on_commit_callbacks) -> None: + if not connection.features.supports_transactions: + pytest.skip("transactions required for this test") + + scratch = [] + with django_capture_on_commit_callbacks() as callbacks: + transaction.on_commit(lambda: scratch.append("one")) + assert len(callbacks) == 1 + assert scratch == [] + callbacks[0]() + assert scratch == ["one"] + + scratch = [] + with django_capture_on_commit_callbacks(execute=True) as callbacks: + transaction.on_commit(lambda: scratch.append("two")) + transaction.on_commit(lambda: scratch.append("three")) + assert len(callbacks) == 2 + assert scratch == ["two", "three"] + callbacks[0]() + assert scratch == ["two", "three", "two"] + + +@pytest.mark.django_db(databases=["default", "second"]) +def test_django_capture_on_commit_callbacks_multidb(django_capture_on_commit_callbacks) -> None: + if not connection.features.supports_transactions: + pytest.skip("transactions required for this test") + + scratch = [] + with django_capture_on_commit_callbacks(using="default", execute=True) as callbacks: + transaction.on_commit(lambda: scratch.append("one")) + assert len(callbacks) == 1 + assert scratch == ["one"] + + scratch = [] + with django_capture_on_commit_callbacks(using="second", execute=True) as callbacks: + transaction.on_commit(lambda: scratch.append("two")) # pragma: no cover + assert len(callbacks) == 0 + assert scratch == [] + + scratch = [] + with django_capture_on_commit_callbacks(using="default", execute=True) as callbacks: + transaction.on_commit(lambda: scratch.append("ten")) + transaction.on_commit(lambda: scratch.append("twenty"), using="second") # pragma: no cover + transaction.on_commit(lambda: scratch.append("thirty")) + assert len(callbacks) == 2 + assert scratch == ["ten", "thirty"] + + +@pytest.mark.django_db(transaction=True) +def test_django_capture_on_commit_callbacks_transactional( + django_capture_on_commit_callbacks, +) -> None: + if not connection.features.supports_transactions: + pytest.skip("transactions required for this test") + + # Bad usage: no transaction (executes immediately). + scratch = [] + with django_capture_on_commit_callbacks() as callbacks: + transaction.on_commit(lambda: scratch.append("one")) + assert len(callbacks) == 0 + assert scratch == ["one"] + + class TestSettings: """Tests for the settings fixture, order matters""" From 0be78e9781682dec745be628ec5fc86ef5f8a3e1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 1 Jun 2021 15:41:34 +0200 Subject: [PATCH 094/150] doc: Switch to new IRC channel See https://github.com/pytest-dev/pytest/pull/8722 --- docs/contributing.rst | 7 +++++-- docs/faq.rst | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index b5f1b7b92..a104ac7ce 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -15,8 +15,11 @@ Community The fastest way to get feedback on contributions/bugs is usually to open an issue in the `issue tracker`_. -Discussions also happen via IRC in #pylib on irc.freenode.org. You may also -be interested in following `@andreaspelme`_ on Twitter. +Discussions also happen via IRC in #pytest `on irc.libera.chat +`_ (join using an IRC client, `via webchat +`_, or `via Matrix +`_). +You may also be interested in following `@andreaspelme`_ on Twitter. ************* In a nutshell diff --git a/docs/faq.rst b/docs/faq.rst index a5a92ed6d..68150f037 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -149,7 +149,10 @@ If you think you've found a bug or something that is wrong in the documentation, feel free to `open an issue on the GitHub project`_ for pytest-django. -Direct help can be found in the #pylib IRC channel on irc.freenode.org. +Direct help can be found in the #pytest IRC channel `on irc.libera.chat +`_ (using an IRC client, `via webchat +`_, or `via Matrix +`_). .. _pytest tag: https://stackoverflow.com/search?q=pytest .. _open an issue on the GitHub project: From cf6b229bce0c717bc590d8fd7cef281f3aa2c1fa Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 6 Jun 2021 18:27:04 +0300 Subject: [PATCH 095/150] Release 4.4.0 --- docs/changelog.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9f3e438cc..5b04c3ba4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,17 @@ Changelog ========= +v4.4.0 (2021-06-06) +------------------- + +Improvements +^^^^^^^^^^^^ + +* Add a fixture :fixture:`django_capture_on_commit_callbacks` to capture + :func:`transaction.on_commit() ` callbacks + in tests. + + v4.3.0 (2021-05-15) ------------------- From 924c132e86104e73f76deb784350d272b0ffde47 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Tue, 31 Aug 2021 06:14:18 +1000 Subject: [PATCH 096/150] docs: Fix a few typos There are small typos in: - docs/database.rst - docs/managing_python_path.rst - tests/test_manage_py_scan.py Fixes: - Should read `implicitly` rather than `impliclity`. - Should read `implicitly` rather than `implicilty`. - Should read `explicitly` rather than `explictly`. --- docs/database.rst | 2 +- docs/managing_python_path.rst | 2 +- tests/test_manage_py_scan.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/database.rst b/docs/database.rst index 2f3115991..c79cb4f95 100644 --- a/docs/database.rst +++ b/docs/database.rst @@ -386,7 +386,7 @@ Populate the test database if you don't use transactional or live_server ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you are using the :func:`pytest.mark.django_db` marker or :fixture:`db` -fixture, you probably don't want to explictly handle transactions in your +fixture, you probably don't want to explicitly handle transactions in your tests. In this case, it is sufficient to populate your database only once. You can put code like this in ``conftest.py``:: diff --git a/docs/managing_python_path.rst b/docs/managing_python_path.rst index a5dcd36a4..083e4e364 100644 --- a/docs/managing_python_path.rst +++ b/docs/managing_python_path.rst @@ -5,7 +5,7 @@ Managing the Python path pytest needs to be able to import the code in your project. Normally, when interacting with Django code, the interaction happens via ``manage.py``, which -will implicilty add that directory to the Python path. +will implicitly add that directory to the Python path. However, when Python is started via the ``pytest`` command, some extra care is needed to have the Python path setup properly. There are two ways to handle diff --git a/tests/test_manage_py_scan.py b/tests/test_manage_py_scan.py index 071a4e0da..395445897 100644 --- a/tests/test_manage_py_scan.py +++ b/tests/test_manage_py_scan.py @@ -4,9 +4,9 @@ @pytest.mark.django_project(project_root="django_project_root", create_manage_py=True) def test_django_project_found(django_testdir) -> None: # XXX: Important: Do not chdir() to django_project_root since runpytest_subprocess - # will call "python /path/to/pytest.py", which will impliclity add cwd to + # will call "python /path/to/pytest.py", which will implicitly add cwd to # the path. By instead calling "python /path/to/pytest.py - # django_project_root", we avoid impliclity adding the project to sys.path + # django_project_root", we avoid implicitly adding the project to sys.path # This matches the behaviour when pytest is called directly as an # executable (cwd is not added to the Python path) From 2e7c96aae8e26dff2238697b08d8394221c5d3dc Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Wed, 15 Sep 2021 14:44:55 +0100 Subject: [PATCH 097/150] Use correct return type for assertTemplateUsed --- pytest_django/asserts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pytest_django/asserts.py b/pytest_django/asserts.py index ab8a5dc53..14aeb1764 100644 --- a/pytest_django/asserts.py +++ b/pytest_django/asserts.py @@ -41,6 +41,7 @@ def assertion_func(*args, **kwargs): if TYPE_CHECKING: from django.http import HttpResponse + from django.test.testcases import _AssertTemplateUsedContext def assertRedirects( response: HttpResponse, @@ -102,7 +103,7 @@ def assertTemplateUsed( template_name: Optional[str] = ..., msg_prefix: str = ..., count: Optional[int] = ..., - ) -> None: + ) -> _AssertTemplateUsedContext: ... def assertTemplateNotUsed( From aade2b54719eb9e8a3f9e8f943394b04ecb5d7f5 Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Wed, 15 Sep 2021 14:48:41 +0100 Subject: [PATCH 098/150] Add correct return type for assertTemplateNotUsed --- pytest_django/asserts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytest_django/asserts.py b/pytest_django/asserts.py index 14aeb1764..05570b0cf 100644 --- a/pytest_django/asserts.py +++ b/pytest_django/asserts.py @@ -41,7 +41,7 @@ def assertion_func(*args, **kwargs): if TYPE_CHECKING: from django.http import HttpResponse - from django.test.testcases import _AssertTemplateUsedContext + from django.test.testcases import _AssertTemplateNotUsedContext, _AssertTemplateUsedContext def assertRedirects( response: HttpResponse, @@ -110,7 +110,7 @@ def assertTemplateNotUsed( response: Optional[HttpResponse] = ..., template_name: Optional[str] = ..., msg_prefix: str = ..., - ) -> None: + ) -> _AssertTemplateNotUsedContext: ... def assertRaisesMessage( From 6ac1495c17d94db1293659edb01966c278064114 Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Fri, 24 Sep 2021 17:07:07 +0100 Subject: [PATCH 099/150] Prefer incomplete types to potential fragility --- pytest_django/asserts.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pytest_django/asserts.py b/pytest_django/asserts.py index 05570b0cf..15380fa47 100644 --- a/pytest_django/asserts.py +++ b/pytest_django/asserts.py @@ -41,7 +41,6 @@ def assertion_func(*args, **kwargs): if TYPE_CHECKING: from django.http import HttpResponse - from django.test.testcases import _AssertTemplateNotUsedContext, _AssertTemplateUsedContext def assertRedirects( response: HttpResponse, @@ -103,14 +102,14 @@ def assertTemplateUsed( template_name: Optional[str] = ..., msg_prefix: str = ..., count: Optional[int] = ..., - ) -> _AssertTemplateUsedContext: + ): ... def assertTemplateNotUsed( response: Optional[HttpResponse] = ..., template_name: Optional[str] = ..., msg_prefix: str = ..., - ) -> _AssertTemplateNotUsedContext: + ): ... def assertRaisesMessage( From 3d0aa2657fd0558292f312a3b4eef579b89893dc Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Thu, 14 Oct 2021 11:56:17 +0200 Subject: [PATCH 100/150] Update mypy to 0.910 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 834115290..27ac395a5 100644 --- a/tox.ini +++ b/tox.ini @@ -53,7 +53,7 @@ commands = extras = deps = flake8 - mypy==0.812 + mypy==0.910 commands = flake8 --version flake8 --statistics {posargs:pytest_django pytest_django_test tests} From d74114eb0d6ae8f48d3778a53866fdc95c638c6d Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Thu, 14 Oct 2021 17:41:08 +0200 Subject: [PATCH 101/150] Fix isort configuration --- Makefile | 2 +- setup.cfg | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7ba0e09fa..ff84d7df5 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ docs: # See setup.cfg for configuration. isort: - find pytest_django tests -name '*.py' -exec isort {} + + isort pytest_django pytest_django_test tests clean: rm -rf bin include/ lib/ man/ pytest_django.egg-info/ build/ diff --git a/setup.cfg b/setup.cfg index b25c69be2..ddaffdabb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -73,6 +73,12 @@ exclude = lib/,src/,docs/,bin/ [isort] forced_separate = tests,pytest_django,pytest_django_test +combine_as_imports = true +default_section = THIRDPARTY +include_trailing_comma = true +known_first_party = formtools +line_length = 79 +multi_line_output = 5 [mypy] check_untyped_defs = True From f2e80f69ca0d29a70725e053a1d1d1b524f22f44 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Thu, 14 Oct 2021 17:43:37 +0200 Subject: [PATCH 102/150] Fix isort errors. --- pytest_django/asserts.py | 5 ++- pytest_django/fixtures.py | 8 +++-- pytest_django/lazy_django.py | 2 +- pytest_django/live_server_helper.py | 2 +- pytest_django/plugin.py | 35 +++++++++++---------- pytest_django_test/db_helpers.py | 4 +-- pytest_django_test/settings_mysql_innodb.py | 1 - pytest_django_test/settings_mysql_myisam.py | 1 - pytest_django_test/settings_sqlite.py | 1 - pytest_django_test/urls_overridden.py | 2 +- tests/conftest.py | 7 ++--- tests/test_asserts.py | 8 +++-- tests/test_db_setup.py | 5 +-- tests/test_django_settings_module.py | 1 - tests/test_environment.py | 3 +- tests/test_fixtures.py | 4 +-- 16 files changed, 41 insertions(+), 48 deletions(-) diff --git a/pytest_django/asserts.py b/pytest_django/asserts.py index 15380fa47..16109c924 100644 --- a/pytest_django/asserts.py +++ b/pytest_django/asserts.py @@ -1,12 +1,11 @@ """ Dynamically load all Django assertion cases and expose them for importing. """ -from typing import Any, Callable, Optional, Sequence, Set, Union from functools import wraps +from typing import Any, Callable, Optional, Sequence, Set, Union from django.test import ( - TestCase, SimpleTestCase, - LiveServerTestCase, TransactionTestCase + LiveServerTestCase, SimpleTestCase, TestCase, TransactionTestCase, ) TYPE_CHECKING = False diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index b58aadeba..307aa251e 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -1,14 +1,16 @@ """All pytest-django fixtures""" -from typing import Any, Callable, Generator, Iterable, List, Optional, Tuple, Union import os from contextlib import contextmanager from functools import partial +from typing import ( + Any, Callable, Generator, Iterable, List, Optional, Tuple, Union, +) import pytest from . import live_server_helper from .django_compat import is_django_unittest -from .lazy_django import skip_if_no_django, get_django_version +from .lazy_django import get_django_version, skip_if_no_django TYPE_CHECKING = False if TYPE_CHECKING: @@ -155,8 +157,8 @@ def _django_db_fixture_helper( django_db_blocker.unblock() request.addfinalizer(django_db_blocker.restore) - import django.test import django.db + import django.test if transactional: test_case_class = django.test.TransactionTestCase diff --git a/pytest_django/lazy_django.py b/pytest_django/lazy_django.py index c71356534..6cf854914 100644 --- a/pytest_django/lazy_django.py +++ b/pytest_django/lazy_django.py @@ -1,9 +1,9 @@ """ Helpers to load Django lazily when Django settings can't be configured. """ -from typing import Any, Tuple import os import sys +from typing import Any, Tuple import pytest diff --git a/pytest_django/live_server_helper.py b/pytest_django/live_server_helper.py index de5f3635a..943198d7c 100644 --- a/pytest_django/live_server_helper.py +++ b/pytest_django/live_server_helper.py @@ -1,4 +1,4 @@ -from typing import Dict, Any +from typing import Any, Dict class LiveServer: diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 3e9dd9c68..9c6523282 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -4,43 +4,42 @@ test database and provides some useful text fixtures. """ -from typing import Generator, List, Optional, Tuple, Union import contextlib import inspect -from functools import reduce import os import pathlib import sys +from functools import reduce +from typing import Generator, List, Optional, Tuple, Union import pytest from .django_compat import is_django_unittest # noqa -from .fixtures import django_assert_num_queries # noqa -from .fixtures import django_assert_max_num_queries # noqa -from .fixtures import django_db_setup # noqa -from .fixtures import django_db_use_migrations # noqa -from .fixtures import django_db_keepdb # noqa -from .fixtures import django_db_createdb # noqa -from .fixtures import django_db_modify_db_settings # noqa -from .fixtures import django_db_modify_db_settings_parallel_suffix # noqa -from .fixtures import django_db_modify_db_settings_tox_suffix # noqa -from .fixtures import django_db_modify_db_settings_xdist_suffix # noqa -from .fixtures import django_capture_on_commit_callbacks # noqa from .fixtures import _live_server_helper # noqa from .fixtures import admin_client # noqa from .fixtures import admin_user # noqa from .fixtures import async_client # noqa +from .fixtures import async_rf # noqa from .fixtures import client # noqa from .fixtures import db # noqa +from .fixtures import django_assert_max_num_queries # noqa +from .fixtures import django_assert_num_queries # noqa +from .fixtures import django_capture_on_commit_callbacks # noqa +from .fixtures import django_db_createdb # noqa +from .fixtures import django_db_keepdb # noqa +from .fixtures import django_db_modify_db_settings # noqa +from .fixtures import django_db_modify_db_settings_parallel_suffix # noqa +from .fixtures import django_db_modify_db_settings_tox_suffix # noqa +from .fixtures import django_db_modify_db_settings_xdist_suffix # noqa +from .fixtures import django_db_reset_sequences # noqa +from .fixtures import django_db_setup # noqa +from .fixtures import django_db_use_migrations # noqa from .fixtures import django_user_model # noqa from .fixtures import django_username_field # noqa from .fixtures import live_server # noqa -from .fixtures import django_db_reset_sequences # noqa -from .fixtures import async_rf # noqa from .fixtures import rf # noqa from .fixtures import settings # noqa from .fixtures import transactional_db # noqa - from .lazy_django import django_settings_is_configured, skip_if_no_django TYPE_CHECKING = False @@ -416,7 +415,9 @@ def django_test_environment(request) -> None: """ if django_settings_is_configured(): _setup_django() - from django.test.utils import setup_test_environment, teardown_test_environment + from django.test.utils import ( + setup_test_environment, teardown_test_environment, + ) debug_ini = request.config.getini("django_debug_mode") if debug_ini == "keep": diff --git a/pytest_django_test/db_helpers.py b/pytest_django_test/db_helpers.py index a451ba86a..6f8d90a53 100644 --- a/pytest_django_test/db_helpers.py +++ b/pytest_django_test/db_helpers.py @@ -1,13 +1,11 @@ import os -import subprocess import sqlite3 +import subprocess import pytest - from django.conf import settings from django.utils.encoding import force_str - # Construct names for the "inner" database used in runpytest tests _settings = settings.DATABASES["default"] diff --git a/pytest_django_test/settings_mysql_innodb.py b/pytest_django_test/settings_mysql_innodb.py index 062cfac03..2f8cb4a1d 100644 --- a/pytest_django_test/settings_mysql_innodb.py +++ b/pytest_django_test/settings_mysql_innodb.py @@ -2,7 +2,6 @@ from .settings_base import * # noqa: F401 F403 - DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", diff --git a/pytest_django_test/settings_mysql_myisam.py b/pytest_django_test/settings_mysql_myisam.py index d939b7cb9..e28ca59c7 100644 --- a/pytest_django_test/settings_mysql_myisam.py +++ b/pytest_django_test/settings_mysql_myisam.py @@ -2,7 +2,6 @@ from .settings_base import * # noqa: F401 F403 - DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", diff --git a/pytest_django_test/settings_sqlite.py b/pytest_django_test/settings_sqlite.py index f58cc7d89..15268bb45 100644 --- a/pytest_django_test/settings_sqlite.py +++ b/pytest_django_test/settings_sqlite.py @@ -1,6 +1,5 @@ from .settings_base import * # noqa: F401 F403 - DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", diff --git a/pytest_django_test/urls_overridden.py b/pytest_django_test/urls_overridden.py index 255a2ca9a..f3ffdc88c 100644 --- a/pytest_django_test/urls_overridden.py +++ b/pytest_django_test/urls_overridden.py @@ -1,5 +1,5 @@ -from django.urls import path from django.http import HttpResponse +from django.urls import path urlpatterns = [ path("overridden_url/", lambda r: HttpResponse("Overridden urlconf works!")) diff --git a/tests/conftest.py b/tests/conftest.py index 414f3035f..b5e5e9e26 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,12 @@ -from typing import Optional import copy +import pathlib import shutil from textwrap import dedent -import pathlib +from typing import Optional import pytest from django.conf import settings - pytest_plugins = "pytester" REPOSITORY_ROOT = pathlib.Path(__file__).parent @@ -40,7 +39,7 @@ def testdir(testdir, monkeypatch): @pytest.fixture(scope="function") def django_testdir(request, testdir, monkeypatch): from pytest_django_test.db_helpers import ( - DB_NAME, TEST_DB_NAME, SECOND_DB_NAME, SECOND_TEST_DB_NAME, + DB_NAME, SECOND_DB_NAME, SECOND_TEST_DB_NAME, TEST_DB_NAME, ) marker = request.node.get_closest_marker("django_project") diff --git a/tests/test_asserts.py b/tests/test_asserts.py index 01b3b0603..0434f1682 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -1,12 +1,12 @@ """ Tests the dynamic loading of all Django assertion cases. """ -from typing import List import inspect +from typing import List import pytest -import pytest_django +import pytest_django from pytest_django.asserts import __all__ as asserts_all @@ -14,9 +14,10 @@ def _get_actual_assertions_names() -> List[str]: """ Returns list with names of all assertion helpers in Django. """ - from django.test import TestCase as DjangoTestCase from unittest import TestCase as DefaultTestCase + from django.test import TestCase as DjangoTestCase + obj = DjangoTestCase('run') def is_assert(func) -> bool: @@ -42,6 +43,7 @@ def test_django_asserts_available() -> None: @pytest.mark.django_db def test_sanity() -> None: from django.http import HttpResponse + from pytest_django.asserts import assertContains, assertNumQueries response = HttpResponse('My response') diff --git a/tests/test_db_setup.py b/tests/test_db_setup.py index c22d9a8dd..14174b4ef 100644 --- a/tests/test_db_setup.py +++ b/tests/test_db_setup.py @@ -1,10 +1,7 @@ import pytest from pytest_django_test.db_helpers import ( - db_exists, - drop_database, - mark_database, - mark_exists, + db_exists, drop_database, mark_database, mark_exists, skip_if_sqlite_in_memory, ) diff --git a/tests/test_django_settings_module.py b/tests/test_django_settings_module.py index fb008e12f..313abf0ad 100644 --- a/tests/test_django_settings_module.py +++ b/tests/test_django_settings_module.py @@ -5,7 +5,6 @@ import pytest - BARE_SETTINGS = """ # At least one database must be configured DATABASES = { diff --git a/tests/test_environment.py b/tests/test_environment.py index a237bd908..776b6f2cd 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -1,15 +1,14 @@ import os import pytest -from django.contrib.sites.models import Site from django.contrib.sites import models as site_models +from django.contrib.sites.models import Site from django.core import mail from django.db import connection from django.test import TestCase from pytest_django_test.app.models import Item - # It doesn't matter which order all the _again methods are run, we just need # to check the environment remains constant. # This is possible with some of the testdir magic, but this is the lazy way diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index a833d32bd..427c6c8a7 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -3,9 +3,9 @@ Not quite all fixtures are tested here, the db and transactional_db fixtures are tested in test_database. """ -from typing import Generator import socket from contextlib import contextmanager +from typing import Generator from urllib.error import HTTPError from urllib.request import urlopen @@ -16,8 +16,8 @@ from django.test.client import Client, RequestFactory from django.utils.encoding import force_str -from pytest_django_test.app.models import Item from pytest_django.lazy_django import get_django_version +from pytest_django_test.app.models import Item @contextmanager From 864ef612132ec5379d8f6ead531d6bceaf8b3b01 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Thu, 14 Oct 2021 17:46:15 +0200 Subject: [PATCH 103/150] Add isort to tox checkqa --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 27ac395a5..07bbe1925 100644 --- a/tox.ini +++ b/tox.ini @@ -54,10 +54,12 @@ extras = deps = flake8 mypy==0.910 + isort commands = flake8 --version flake8 --statistics {posargs:pytest_django pytest_django_test tests} mypy {posargs:pytest_django pytest_django_test tests} + isort --check-only --diff pytest_django pytest_django_test tests [testenv:doc8] extras = From 4e125e16644a3f88a8f8f9e34f9528c7b0a7abea Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 17 Oct 2021 18:06:49 +0300 Subject: [PATCH 104/150] isort: two lines after imports That's the style I'm used to. --- pytest_django/asserts.py | 1 + pytest_django/fixtures.py | 1 + pytest_django/plugin.py | 1 + pytest_django_test/db_helpers.py | 1 + pytest_django_test/settings_mysql_innodb.py | 1 + pytest_django_test/settings_mysql_myisam.py | 1 + pytest_django_test/settings_postgres.py | 1 + pytest_django_test/settings_sqlite.py | 1 + pytest_django_test/settings_sqlite_file.py | 1 + pytest_django_test/urls.py | 1 + pytest_django_test/urls_overridden.py | 1 + setup.cfg | 2 +- tests/conftest.py | 1 + tests/test_django_configurations.py | 1 + tests/test_django_settings_module.py | 1 + tests/test_environment.py | 1 + 16 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pytest_django/asserts.py b/pytest_django/asserts.py index 16109c924..ff2102adb 100644 --- a/pytest_django/asserts.py +++ b/pytest_django/asserts.py @@ -8,6 +8,7 @@ LiveServerTestCase, SimpleTestCase, TestCase, TransactionTestCase, ) + TYPE_CHECKING = False diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 307aa251e..7fcfe679e 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -12,6 +12,7 @@ from .django_compat import is_django_unittest from .lazy_django import get_django_version, skip_if_no_django + TYPE_CHECKING = False if TYPE_CHECKING: from typing import Literal diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 9c6523282..b006305c8 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -42,6 +42,7 @@ from .fixtures import transactional_db # noqa from .lazy_django import django_settings_is_configured, skip_if_no_django + TYPE_CHECKING = False if TYPE_CHECKING: from typing import ContextManager, NoReturn diff --git a/pytest_django_test/db_helpers.py b/pytest_django_test/db_helpers.py index 6f8d90a53..6497e237c 100644 --- a/pytest_django_test/db_helpers.py +++ b/pytest_django_test/db_helpers.py @@ -6,6 +6,7 @@ from django.conf import settings from django.utils.encoding import force_str + # Construct names for the "inner" database used in runpytest tests _settings = settings.DATABASES["default"] diff --git a/pytest_django_test/settings_mysql_innodb.py b/pytest_django_test/settings_mysql_innodb.py index 2f8cb4a1d..062cfac03 100644 --- a/pytest_django_test/settings_mysql_innodb.py +++ b/pytest_django_test/settings_mysql_innodb.py @@ -2,6 +2,7 @@ from .settings_base import * # noqa: F401 F403 + DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", diff --git a/pytest_django_test/settings_mysql_myisam.py b/pytest_django_test/settings_mysql_myisam.py index e28ca59c7..d939b7cb9 100644 --- a/pytest_django_test/settings_mysql_myisam.py +++ b/pytest_django_test/settings_mysql_myisam.py @@ -2,6 +2,7 @@ from .settings_base import * # noqa: F401 F403 + DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", diff --git a/pytest_django_test/settings_postgres.py b/pytest_django_test/settings_postgres.py index af742433e..2661fbc5a 100644 --- a/pytest_django_test/settings_postgres.py +++ b/pytest_django_test/settings_postgres.py @@ -2,6 +2,7 @@ from .settings_base import * # noqa: F401 F403 + # PyPy compatibility try: from psycopg2cffi import compat diff --git a/pytest_django_test/settings_sqlite.py b/pytest_django_test/settings_sqlite.py index 15268bb45..f58cc7d89 100644 --- a/pytest_django_test/settings_sqlite.py +++ b/pytest_django_test/settings_sqlite.py @@ -1,5 +1,6 @@ from .settings_base import * # noqa: F401 F403 + DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", diff --git a/pytest_django_test/settings_sqlite_file.py b/pytest_django_test/settings_sqlite_file.py index 02395561a..4f3404a09 100644 --- a/pytest_django_test/settings_sqlite_file.py +++ b/pytest_django_test/settings_sqlite_file.py @@ -2,6 +2,7 @@ from .settings_base import * # noqa: F401 F403 + # This is a SQLite configuration, which uses a file based database for # tests (via setting TEST_NAME / TEST['NAME']). diff --git a/pytest_django_test/urls.py b/pytest_django_test/urls.py index 363b979c5..956dcef93 100644 --- a/pytest_django_test/urls.py +++ b/pytest_django_test/urls.py @@ -2,6 +2,7 @@ from .app import views + urlpatterns = [ path("item_count/", views.item_count), path("admin-required/", views.admin_required_view), diff --git a/pytest_django_test/urls_overridden.py b/pytest_django_test/urls_overridden.py index f3ffdc88c..b84507fed 100644 --- a/pytest_django_test/urls_overridden.py +++ b/pytest_django_test/urls_overridden.py @@ -1,6 +1,7 @@ from django.http import HttpResponse from django.urls import path + urlpatterns = [ path("overridden_url/", lambda r: HttpResponse("Overridden urlconf works!")) ] diff --git a/setup.cfg b/setup.cfg index ddaffdabb..df32ec0f8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -76,9 +76,9 @@ forced_separate = tests,pytest_django,pytest_django_test combine_as_imports = true default_section = THIRDPARTY include_trailing_comma = true -known_first_party = formtools line_length = 79 multi_line_output = 5 +lines_after_imports = 2 [mypy] check_untyped_defs = True diff --git a/tests/conftest.py b/tests/conftest.py index b5e5e9e26..beb6cfff2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,7 @@ import pytest from django.conf import settings + pytest_plugins = "pytester" REPOSITORY_ROOT = pathlib.Path(__file__).parent diff --git a/tests/test_django_configurations.py b/tests/test_django_configurations.py index d5941b8e9..e8d3e8add 100644 --- a/tests/test_django_configurations.py +++ b/tests/test_django_configurations.py @@ -4,6 +4,7 @@ """ import pytest + pytest.importorskip("configurations") diff --git a/tests/test_django_settings_module.py b/tests/test_django_settings_module.py index 313abf0ad..fb008e12f 100644 --- a/tests/test_django_settings_module.py +++ b/tests/test_django_settings_module.py @@ -5,6 +5,7 @@ import pytest + BARE_SETTINGS = """ # At least one database must be configured DATABASES = { diff --git a/tests/test_environment.py b/tests/test_environment.py index 776b6f2cd..450847fce 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -9,6 +9,7 @@ from pytest_django_test.app.models import Item + # It doesn't matter which order all the _again methods are run, we just need # to check the environment remains constant. # This is possible with some of the testdir magic, but this is the lazy way From ac76775fa0a931307eb5f90cee11e4c6cf4aebb8 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Thu, 4 Nov 2021 16:45:41 +0100 Subject: [PATCH 105/150] Add Python 3.10 to test matrix. --- .github/workflows/main.yml | 4 ++++ setup.cfg | 1 + tox.ini | 1 + 3 files changed, 6 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1c670c797..18f30097e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -55,6 +55,10 @@ jobs: python: 3.8 allow_failure: false + - name: py310-dj32-postgres-xdist-coverage + python: '3.10' + allow_failure: false + - name: py39-dj32-postgres-xdist-coverage python: 3.9 allow_failure: false diff --git a/setup.cfg b/setup.cfg index df32ec0f8..54813ab5f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Software Development :: Testing diff --git a/tox.ini b/tox.ini index 07bbe1925..bef480670 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] envlist = + py310-dj32-postgres py39-dj{32,31,30,22}-postgres py38-dj{32,31,30,22}-postgres py37-dj{32,31,30,22}-postgres From c5465adbf6fa9cde4a00d3cacc2246d575430b54 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Fri, 5 Nov 2021 10:21:57 +0100 Subject: [PATCH 106/150] Add Django main to tox envlist --- pytest_django/fixtures.py | 4 ++++ tox.ini | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 7fcfe679e..1eb5949ee 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -144,6 +144,8 @@ def _django_db_fixture_helper( transactional: bool = False, reset_sequences: bool = False, ) -> None: + from django import VERSION + if is_django_unittest(request): return @@ -175,6 +177,8 @@ class PytestDjangoTestCase(test_case_class): # type: ignore[misc,valid-type] databases = _databases PytestDjangoTestCase.setUpClass() + if VERSION >= (4, 0): + request.addfinalizer(PytestDjangoTestCase.doClassCleanups) request.addfinalizer(PytestDjangoTestCase.tearDownClass) test_case = PytestDjangoTestCase(methodName="__init__") diff --git a/tox.ini b/tox.ini index bef480670..8ac69466d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,8 @@ [tox] envlist = - py310-dj32-postgres - py39-dj{32,31,30,22}-postgres - py38-dj{32,31,30,22}-postgres + py310-dj{main,32}-postgres + py39-dj{main,32,31,30,22}-postgres + py38-dj{main,32,31,30,22}-postgres py37-dj{32,31,30,22}-postgres py36-dj{32,31,30,22}-postgres py35-dj{22}-postgres From f23a20558f632cf525a2c888c4651ee9824e7661 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Tue, 16 Nov 2021 14:29:19 +0100 Subject: [PATCH 107/150] Add USE_TZ to test settings. (#958) --- pytest_django_test/settings_base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pytest_django_test/settings_base.py b/pytest_django_test/settings_base.py index ff8dc2d39..d1694cd28 100644 --- a/pytest_django_test/settings_base.py +++ b/pytest_django_test/settings_base.py @@ -29,3 +29,5 @@ ] DATABASE_ROUTERS = ['pytest_django_test.db_router.DbRouter'] + +USE_TZ = True From 4a0475d6db899056dccbea899d6eef23e4dbaf06 Mon Sep 17 00:00:00 2001 From: Yassine-cheffai Date: Sat, 6 Nov 2021 12:25:01 +0100 Subject: [PATCH 108/150] fix DJANGO_SETTINGS_MODULE in main documentation using point to sepcify the path to the settings file, instead of underscore --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 9b810e5ca..6e73860d2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -23,7 +23,7 @@ Make sure ``DJANGO_SETTINGS_MODULE`` is defined (see # -- FILE: pytest.ini (or tox.ini) [pytest] - DJANGO_SETTINGS_MODULE = test_settings + DJANGO_SETTINGS_MODULE = test.settings # -- recommended but optional: python_files = tests.py test_*.py *_tests.py From cb914220e0a9843376cd4126e457e02e0ae53847 Mon Sep 17 00:00:00 2001 From: Yassine-cheffai Date: Sat, 6 Nov 2021 12:22:50 +0100 Subject: [PATCH 109/150] fix DJANGO_SETTINGS_MODULE docs using point to sepcify the path to the setting file, instead of underscore --- docs/configuring_django.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/configuring_django.rst b/docs/configuring_django.rst index ab4a4c980..35ee0a452 100644 --- a/docs/configuring_django.rst +++ b/docs/configuring_django.rst @@ -14,12 +14,12 @@ Django settings the same way Django does by default. Example:: - $ export DJANGO_SETTINGS_MODULE=test_settings + $ export DJANGO_SETTINGS_MODULE=test.settings $ pytest or:: - $ DJANGO_SETTINGS_MODULE=test_settings pytest + $ DJANGO_SETTINGS_MODULE=test.settings pytest Command line option ``--ds=SETTINGS`` @@ -27,7 +27,7 @@ Command line option ``--ds=SETTINGS`` Example:: - $ pytest --ds=test_settings + $ pytest --ds=test.settings ``pytest.ini`` settings @@ -36,7 +36,7 @@ Example:: Example contents of pytest.ini:: [pytest] - DJANGO_SETTINGS_MODULE = test_settings + DJANGO_SETTINGS_MODULE = test.settings Order of choosing settings -------------------------- From dcd740c5fb32d95c567c3f9c09e7f94a96e20c6c Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Wed, 17 Nov 2021 15:12:07 +0100 Subject: [PATCH 110/150] Drop Django 3.0 support. --- .github/workflows/main.yml | 6 +----- README.rst | 2 +- setup.cfg | 1 - tox.ini | 9 ++++----- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 18f30097e..1a9ce7e01 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -67,7 +67,7 @@ jobs: python: 3.9 allow_failure: false - - name: py37-dj30-mysql_innodb-coverage + - name: py37-dj31-mysql_innodb-coverage python: 3.7 allow_failure: false @@ -79,10 +79,6 @@ jobs: python: 3.7 allow_failure: false - - name: py38-dj30-sqlite-xdist-coverage - python: 3.8 - allow_failure: false - - name: py38-dj32-sqlite-xdist-coverage python: 3.8 allow_failure: false diff --git a/README.rst b/README.rst index 94774fba6..ece0d06e8 100644 --- a/README.rst +++ b/README.rst @@ -28,7 +28,7 @@ pytest-django allows you to test your Django project/applications with the `_ * Version compatibility: - * Django: 2.2, 3.0, 3.1, 3.2 and latest main branch (compatible at the time of + * Django: 2.2, 3.1, 3.2 and latest main branch (compatible at the time of each release) * Python: CPython>=3.5 or PyPy 3 * pytest: >=5.4 diff --git a/setup.cfg b/setup.cfg index 54813ab5f..2d70f88b8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,6 @@ classifiers = Development Status :: 5 - Production/Stable Framework :: Django Framework :: Django :: 2.2 - Framework :: Django :: 3.0 Framework :: Django :: 3.1 Framework :: Django :: 3.2 Intended Audience :: Developers diff --git a/tox.ini b/tox.ini index 8ac69466d..e084750c5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,10 @@ [tox] envlist = py310-dj{main,32}-postgres - py39-dj{main,32,31,30,22}-postgres - py38-dj{main,32,31,30,22}-postgres - py37-dj{32,31,30,22}-postgres - py36-dj{32,31,30,22}-postgres + py39-dj{main,32,31,22}-postgres + py38-dj{main,32,31,22}-postgres + py37-dj{32,31,22}-postgres + py36-dj{32,31,22}-postgres py35-dj{22}-postgres checkqa @@ -14,7 +14,6 @@ deps = djmain: https://github.com/django/django/archive/main.tar.gz dj32: Django>=3.2,<4.0 dj31: Django>=3.1,<3.2 - dj30: Django>=3.0,<3.1 dj22: Django>=2.2,<2.3 mysql_myisam: mysqlclient==1.4.2.post1 From df0abaa057bec479856c71011a45433b3790556a Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Wed, 17 Nov 2021 22:18:52 +0100 Subject: [PATCH 111/150] Add Supported Django versions badge to README.rst --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index ece0d06e8..261cd1336 100644 --- a/README.rst +++ b/README.rst @@ -10,6 +10,10 @@ :alt: Build Status :target: https://github.com/pytest-dev/pytest-django/actions +.. image:: https://img.shields.io/pypi/djversions/pytest-django.svg + :alt: Supported Django versions + :target: https://pypi.org/project/pytest-django/ + .. image:: https://img.shields.io/codecov/c/github/pytest-dev/pytest-django.svg?style=flat :alt: Coverage :target: https://codecov.io/gh/pytest-dev/pytest-django From d2beb3f00c539e73fb4f31c1dac058e003f9b5bb Mon Sep 17 00:00:00 2001 From: Mathieu Kniewallner Date: Sun, 21 Nov 2021 21:51:04 +0100 Subject: [PATCH 112/150] Add test for mirror database --- tests/test_database.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_database.py b/tests/test_database.py index 9b5a88bdc..f51819553 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -249,6 +249,14 @@ def test_replica_database_not_allowed(self, request) -> None: with pytest.raises(AssertionError, match='not allowed'): Item.objects.count() + @pytest.mark.django_db(transaction=True, databases=['default', 'replica']) + def test_replica_mirrors_default_database(self, request) -> None: + Item.objects.create(name='spam') + Item.objects.using('replica').create(name='spam') + + assert Item.objects.count() == 2 + assert Item.objects.using('replica').count() == 2 + @pytest.mark.django_db(databases='__all__') def test_all_databases(self, request) -> None: Item.objects.count() From f98e99e4506b43e9da9724d0167d5227360a178c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 27 Nov 2021 21:47:37 +0200 Subject: [PATCH 113/150] Change "native migrations" to just "migrations" These days just "migrations" is understood to be the built-in Django migrations. --- pytest_django/fixtures.py | 4 ++-- tests/test_db_setup.py | 15 +++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 1eb5949ee..a8f990634 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -111,7 +111,7 @@ def django_db_setup( setup_databases_args = {} if not django_db_use_migrations: - _disable_native_migrations() + _disable_migrations() if django_db_keepdb and not django_db_createdb: setup_databases_args["keepdb"] = True @@ -186,7 +186,7 @@ class PytestDjangoTestCase(test_case_class): # type: ignore[misc,valid-type] request.addfinalizer(test_case._post_teardown) -def _disable_native_migrations() -> None: +def _disable_migrations() -> None: from django.conf import settings from django.core.management.commands import migrate diff --git a/tests/test_db_setup.py b/tests/test_db_setup.py index 14174b4ef..31cb2452d 100644 --- a/tests/test_db_setup.py +++ b/tests/test_db_setup.py @@ -449,8 +449,8 @@ def test_a(): result.stdout.fnmatch_lines(["*PASSED*test_a*"]) -class TestNativeMigrations: - """ Tests for Django Migrations """ +class TestMigrations: + """Tests for Django Migrations.""" def test_no_migrations(self, django_testdir) -> None: django_testdir.create_test_module( @@ -464,12 +464,11 @@ def test_inner_migrations(): """ ) - migration_file = django_testdir.project_root.join( - "tpkg/app/migrations/0001_initial.py" - ) - assert migration_file.isfile() - migration_file.write( - 'raise Exception("This should not get imported.")', ensure=True + django_testdir.create_test_module( + """ + raise Exception("This should not get imported.") + """, + "migrations/0001_initial.py", ) result = django_testdir.runpytest_subprocess( From 685e9f6d2cb734d93690356c6f8c568e6f0828e2 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 26 Nov 2021 12:50:02 +0200 Subject: [PATCH 114/150] Add pyproject.toml file Avoid some deprecation warnings about setup.cfg setup_requires. --- pyproject.toml | 11 +++++++++++ setup.cfg | 5 +---- setup.py | 7 ++----- 3 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..6f907ba16 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[build-system] +requires = [ + "setuptools>=45.0", + # sync with setup.cfg until we discard non-pep-517/518 + "setuptools-scm[toml]>=5.0.0", + "wheel", +] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] +write_to = "pytest_django/_version.py" diff --git a/setup.cfg b/setup.cfg index 2d70f88b8..0546efb94 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,7 +36,7 @@ project_urls = [options] packages = pytest_django python_requires = >=3.5 -setup_requires = setuptools_scm>=1.11.1 +setup_requires = setuptools_scm>=5.0.0 install_requires = pytest>=5.4.0 zip_safe = no @@ -62,9 +62,6 @@ addopts = --strict-markers -ra DJANGO_SETTINGS_MODULE = pytest_django_test.settings_sqlite_file testpaths = tests -[wheel] -universal = 0 - [flake8] # W503 line break before binary operator ignore = W503 diff --git a/setup.py b/setup.py index abd9cb67f..7f1a1763c 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,4 @@ from setuptools import setup -setup( - use_scm_version={ - 'write_to': 'pytest_django/_version.py', - }, -) +if __name__ == "__main__": + setup() From 14c0478c999f361135fb40b2373f77f3c729c6d7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 28 Nov 2021 18:04:35 +0200 Subject: [PATCH 115/150] Update reference pytest.Store -> pytest.Stash The name changed. [skip ci] --- pytest_django/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index b006305c8..1536f1afd 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -461,7 +461,7 @@ def _django_db_marker(request) -> None: if marker: transaction, reset_sequences, databases = validate_django_db(marker) - # TODO: Use pytest Store (item.store) once that's stable. + # TODO: Use pytest Stash (item.stash) once that's stable. request.node._pytest_django_databases = databases if reset_sequences: From 2a305862be038bf749cbae30bfc9df6506922ed1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 28 Nov 2021 21:01:39 +0200 Subject: [PATCH 116/150] tox: rename checkqa -> linting Match pytest-dev/pytest. --- .github/workflows/main.yml | 2 +- tox.ini | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1a9ce7e01..60816b096 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -51,7 +51,7 @@ jobs: fail-fast: false matrix: include: - - name: checkqa,docs + - name: linting,docs python: 3.8 allow_failure: false diff --git a/tox.ini b/tox.ini index e084750c5..287bd4031 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ envlist = py37-dj{32,31,22}-postgres py36-dj{32,31,22}-postgres py35-dj{22}-postgres - checkqa + linting [testenv] extras = testing @@ -49,7 +49,7 @@ commands = coverage: coverage report coverage: coverage xml -[testenv:checkqa] +[testenv:linting] extras = deps = flake8 From c7e02296d1697ad364e8e55ad9ef674314d70502 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 28 Nov 2021 21:43:30 +0200 Subject: [PATCH 117/150] tests: fix `mark_exists()` check on postgresql with non-standard psqlrc I have `\timing` in my `~/.psqlrc` which prints to stdout even when the table doesn't exist. --- pytest_django_test/db_helpers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pytest_django_test/db_helpers.py b/pytest_django_test/db_helpers.py index 6497e237c..14ebe333a 100644 --- a/pytest_django_test/db_helpers.py +++ b/pytest_django_test/db_helpers.py @@ -166,8 +166,7 @@ def mark_exists(): if db_engine == "postgresql": r = run_psql(TEST_DB_NAME, "-c", "SELECT 1 FROM mark_table") - # When something pops out on std_out, we are good - return bool(r.std_out) + return r.status_code == 0 if db_engine == "mysql": r = run_mysql(TEST_DB_NAME, "-e", "SELECT 1 FROM mark_table") From eeeb163d9ec88c319bab0d70910b5849b63e68c0 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 28 Nov 2021 21:59:55 +0200 Subject: [PATCH 118/150] Fix `@pytest.mark.django_db(reset_sequence=True)` not being sorted as transactional It implies transactional, so should sort as such. --- pytest_django/plugin.py | 41 +++++++++++++++++++++++------------------ tests/test_db_setup.py | 11 ++++++++++- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 1536f1afd..c74d7fc10 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -376,28 +376,33 @@ def get_order_number(test: pytest.Item) -> int: if test_cls: # Beware, TestCase is a subclass of TransactionTestCase if issubclass(test_cls, TestCase): - return 0 - if issubclass(test_cls, TransactionTestCase): - return 1 - - marker_db = test.get_closest_marker('django_db') - if not marker_db: - transaction = None + uses_db = True + transactional = False + elif issubclass(test_cls, TransactionTestCase): + uses_db = True + transactional = True + else: + uses_db = False + transactional = False else: - transaction = validate_django_db(marker_db)[0] - if transaction is True: - return 1 + marker_db = test.get_closest_marker('django_db') + if marker_db: + transaction, reset_sequences, databases = validate_django_db(marker_db) + uses_db = True + transactional = transaction or reset_sequences + else: + uses_db = False + transactional = False + fixtures = getattr(test, 'fixturenames', []) + transactional = transactional or "transactional_db" in fixtures + uses_db = uses_db or "db" in fixtures - fixtures = getattr(test, 'fixturenames', []) - if "transactional_db" in fixtures: + if transactional: return 1 - - if transaction is False: + elif uses_db: return 0 - if "db" in fixtures: - return 0 - - return 2 + else: + return 2 items.sort(key=get_order_number) diff --git a/tests/test_db_setup.py b/tests/test_db_setup.py index 31cb2452d..8f00f0caa 100644 --- a/tests/test_db_setup.py +++ b/tests/test_db_setup.py @@ -42,9 +42,16 @@ def test_run_second_decorator(): def test_run_second_fixture(transactional_db): pass + def test_run_second_reset_sequences_fixture(django_db_reset_sequences): + pass + def test_run_first_fixture(db): pass + @pytest.mark.django_db(reset_sequences=True) + def test_run_second_reset_sequences_decorator(): + pass + @pytest.mark.django_db def test_run_first_decorator(): pass @@ -65,7 +72,7 @@ class MyTransactionTestCase(TransactionTestCase): def test_run_second_transaction_test_case(self): pass ''') - result = django_testdir.runpytest_subprocess('-v', '-s') + result = django_testdir.runpytest_subprocess('-q', '--collect-only') assert result.ret == 0 result.stdout.fnmatch_lines([ "*test_run_first_fixture*", @@ -73,7 +80,9 @@ def test_run_second_transaction_test_case(self): "*test_run_first_django_test_case*", "*test_run_second_decorator*", "*test_run_second_fixture*", + "*test_run_second_reset_sequences_decorator*", "*test_run_second_transaction_test_case*", + "*test_run_second_reset_sequences_fixture*", "*test_run_last_test_case*", "*test_run_last_simple_test_case*", ]) From d6b9563249c86fc51c9359cebc9a1d16eaf0b8f7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 28 Nov 2021 20:02:57 +0200 Subject: [PATCH 119/150] Arrange db fixtures in a more extensible way The current scheme is not conducive to adding additional modifier fixtures similar to ``django_db_reset_sequence``, such as a fixture for ``serialized_rollback`` or for specifying databases. Instead, arrange it such that there is a base helper fixture `_django_db_helper` which does all the work, and the other fixtures merely exist to modify it. --- pytest_django/fixtures.py | 100 +++++++++++++++++++------------------- pytest_django/plugin.py | 36 ++------------ tests/test_db_setup.py | 2 +- 3 files changed, 54 insertions(+), 84 deletions(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index a8f990634..6cb0873ba 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -138,24 +138,30 @@ def teardown_database() -> None: request.addfinalizer(teardown_database) -def _django_db_fixture_helper( +@pytest.fixture() +def _django_db_helper( request, + django_db_setup: None, django_db_blocker, - transactional: bool = False, - reset_sequences: bool = False, ) -> None: from django import VERSION if is_django_unittest(request): return - if not transactional and "live_server" in request.fixturenames: - # Do nothing, we get called with transactional=True, too. - return + marker = request.node.get_closest_marker("django_db") + if marker: + transactional, reset_sequences, _databases = validate_django_db(marker) + else: + transactional, reset_sequences, _databases = False, False, None - _databases = getattr( - request.node, "_pytest_django_databases", None, - ) # type: Optional[_DjangoDbDatabases] + transactional = transactional or ( + "transactional_db" in request.fixturenames + or "live_server" in request.fixturenames + ) + reset_sequences = reset_sequences or ( + "django_db_reset_sequences" in request.fixturenames + ) django_db_blocker.unblock() request.addfinalizer(django_db_blocker.restore) @@ -186,6 +192,26 @@ class PytestDjangoTestCase(test_case_class): # type: ignore[misc,valid-type] request.addfinalizer(test_case._post_teardown) +def validate_django_db(marker) -> "_DjangoDb": + """Validate the django_db marker. + + It checks the signature and creates the ``transaction``, + ``reset_sequences`` and ``databases`` attributes on the marker + which will have the correct values. + + A sequence reset is only allowed when combined with a transaction. + """ + + def apifun( + transaction: bool = False, + reset_sequences: bool = False, + databases: "_DjangoDbDatabases" = None, + ) -> "_DjangoDb": + return transaction, reset_sequences, databases + + return apifun(*marker.args, **marker.kwargs) + + def _disable_migrations() -> None: from django.conf import settings from django.core.management.commands import migrate @@ -229,41 +255,24 @@ def _set_suffix_to_test_databases(suffix: str) -> None: @pytest.fixture(scope="function") -def db( - request, - django_db_setup: None, - django_db_blocker, -) -> None: +def db(_django_db_helper: None) -> None: """Require a django test database. This database will be setup with the default fixtures and will have the transaction management disabled. At the end of the test the outer transaction that wraps the test itself will be rolled back to undo any changes to the database (in case the backend supports transactions). - This is more limited than the ``transactional_db`` resource but + This is more limited than the ``transactional_db`` fixture but faster. - If multiple database fixtures are requested, they take precedence - over each other in the following order (the last one wins): ``db``, - ``transactional_db``, ``django_db_reset_sequences``. + If both ``db`` and ``transactional_db`` are requested, + ``transactional_db`` takes precedence. """ - if "django_db_reset_sequences" in request.fixturenames: - request.getfixturevalue("django_db_reset_sequences") - if ( - "transactional_db" in request.fixturenames - or "live_server" in request.fixturenames - ): - request.getfixturevalue("transactional_db") - else: - _django_db_fixture_helper(request, django_db_blocker, transactional=False) + # The `_django_db_helper` fixture checks if `db` is requested. @pytest.fixture(scope="function") -def transactional_db( - request, - django_db_setup: None, - django_db_blocker, -) -> None: +def transactional_db(_django_db_helper: None) -> None: """Require a django test database with transaction support. This will re-initialise the django database for each test and is @@ -272,35 +281,26 @@ def transactional_db( If you want to use the database with transactions you must request this resource. - If multiple database fixtures are requested, they take precedence - over each other in the following order (the last one wins): ``db``, - ``transactional_db``, ``django_db_reset_sequences``. + If both ``db`` and ``transactional_db`` are requested, + ``transactional_db`` takes precedence. """ - if "django_db_reset_sequences" in request.fixturenames: - request.getfixturevalue("django_db_reset_sequences") - _django_db_fixture_helper(request, django_db_blocker, transactional=True) + # The `_django_db_helper` fixture checks if `transactional_db` is requested. @pytest.fixture(scope="function") def django_db_reset_sequences( - request, - django_db_setup: None, - django_db_blocker, + _django_db_helper: None, + transactional_db: None, ) -> None: """Require a transactional test database with sequence reset support. - This behaves like the ``transactional_db`` fixture, with the addition - of enforcing a reset of all auto increment sequences. If the enquiring + This requests the ``transactional_db`` fixture, and additionally + enforces a reset of all auto increment sequences. If the enquiring test relies on such values (e.g. ids as primary keys), you should request this resource to ensure they are consistent across tests. - - If multiple database fixtures are requested, they take precedence - over each other in the following order (the last one wins): ``db``, - ``transactional_db``, ``django_db_reset_sequences``. """ - _django_db_fixture_helper( - request, django_db_blocker, transactional=True, reset_sequences=True - ) + # The `_django_db_helper` fixture checks if `django_db_reset_sequences` + # is requested. @pytest.fixture() diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index c74d7fc10..63917d62e 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -15,6 +15,7 @@ import pytest from .django_compat import is_django_unittest # noqa +from .fixtures import _django_db_helper # noqa from .fixtures import _live_server_helper # noqa from .fixtures import admin_client # noqa from .fixtures import admin_user # noqa @@ -40,6 +41,7 @@ from .fixtures import rf # noqa from .fixtures import settings # noqa from .fixtures import transactional_db # noqa +from .fixtures import validate_django_db from .lazy_django import django_settings_is_configured, skip_if_no_django @@ -49,8 +51,6 @@ import django - from .fixtures import _DjangoDb, _DjangoDbDatabases - SETTINGS_MODULE_ENV = "DJANGO_SETTINGS_MODULE" CONFIGURATION_ENV = "DJANGO_CONFIGURATION" @@ -464,17 +464,7 @@ def _django_db_marker(request) -> None: """ marker = request.node.get_closest_marker("django_db") if marker: - transaction, reset_sequences, databases = validate_django_db(marker) - - # TODO: Use pytest Stash (item.stash) once that's stable. - request.node._pytest_django_databases = databases - - if reset_sequences: - request.getfixturevalue("django_db_reset_sequences") - elif transaction: - request.getfixturevalue("transactional_db") - else: - request.getfixturevalue("db") + request.getfixturevalue("_django_db_helper") @pytest.fixture(autouse=True, scope="class") @@ -743,26 +733,6 @@ def restore(self) -> None: _blocking_manager = _DatabaseBlocker() -def validate_django_db(marker) -> "_DjangoDb": - """Validate the django_db marker. - - It checks the signature and creates the ``transaction``, - ``reset_sequences`` and ``databases`` attributes on the marker - which will have the correct values. - - A sequence reset is only allowed when combined with a transaction. - """ - - def apifun( - transaction: bool = False, - reset_sequences: bool = False, - databases: "_DjangoDbDatabases" = None, - ) -> "_DjangoDb": - return transaction, reset_sequences, databases - - return apifun(*marker.args, **marker.kwargs) - - def validate_urls(marker) -> List[str]: """Validate the urls marker. diff --git a/tests/test_db_setup.py b/tests/test_db_setup.py index 8f00f0caa..b529965d3 100644 --- a/tests/test_db_setup.py +++ b/tests/test_db_setup.py @@ -80,9 +80,9 @@ def test_run_second_transaction_test_case(self): "*test_run_first_django_test_case*", "*test_run_second_decorator*", "*test_run_second_fixture*", + "*test_run_second_reset_sequences_fixture*", "*test_run_second_reset_sequences_decorator*", "*test_run_second_transaction_test_case*", - "*test_run_second_reset_sequences_fixture*", "*test_run_last_test_case*", "*test_run_last_simple_test_case*", ]) From 3a7ed7ac52f1c49b0892eb13965f855fb2c0e1b8 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 28 Nov 2021 23:22:11 +0200 Subject: [PATCH 120/150] fixtures: slight style improvement --- pytest_django/fixtures.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 6cb0873ba..842545e0f 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -151,9 +151,9 @@ def _django_db_helper( marker = request.node.get_closest_marker("django_db") if marker: - transactional, reset_sequences, _databases = validate_django_db(marker) + transactional, reset_sequences, databases = validate_django_db(marker) else: - transactional, reset_sequences, _databases = False, False, None + transactional, reset_sequences, databases = False, False, None transactional = transactional or ( "transactional_db" in request.fixturenames @@ -175,6 +175,7 @@ def _django_db_helper( test_case_class = django.test.TestCase _reset_sequences = reset_sequences + _databases = databases class PytestDjangoTestCase(test_case_class): # type: ignore[misc,valid-type] if transactional and _reset_sequences: From 8433d5b9da6e6529ceaa85c99ac9a2e56c936184 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 28 Nov 2021 23:13:59 +0200 Subject: [PATCH 121/150] Always set `reset_sequences` on the test class according to the given value This way Django will warn about any misuses and we don't rely on defaults. --- pytest_django/fixtures.py | 3 +-- tests/test_database.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 842545e0f..8c955858c 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -178,8 +178,7 @@ def _django_db_helper( _databases = databases class PytestDjangoTestCase(test_case_class): # type: ignore[misc,valid-type] - if transactional and _reset_sequences: - reset_sequences = True + reset_sequences = _reset_sequences if _databases is not None: databases = _databases diff --git a/tests/test_database.py b/tests/test_database.py index f51819553..9016240b9 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -219,7 +219,7 @@ def test_reset_sequences_disabled(self, request) -> None: marker = request.node.get_closest_marker("django_db") assert not marker.kwargs - @pytest.mark.django_db(reset_sequences=True) + @pytest.mark.django_db(transaction=True, reset_sequences=True) def test_reset_sequences_enabled(self, request) -> None: marker = request.node.get_closest_marker("django_db") assert marker.kwargs["reset_sequences"] From 3e03224dade5dc9e2f1a6f35c554bd2f8c999683 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 28 Nov 2021 23:26:45 +0200 Subject: [PATCH 122/150] Remove now inaccurate statement in `_django_db_marker`'s docstring --- pytest_django/plugin.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 63917d62e..6dc812b21 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -457,11 +457,7 @@ def django_db_blocker() -> "Optional[_DatabaseBlocker]": @pytest.fixture(autouse=True) def _django_db_marker(request) -> None: - """Implement the django_db marker, internal to pytest-django. - - This will dynamically request the ``db``, ``transactional_db`` or - ``django_db_reset_sequences`` fixtures as required by the django_db marker. - """ + """Implement the django_db marker, internal to pytest-django.""" marker = request.node.get_closest_marker("django_db") if marker: request.getfixturevalue("_django_db_helper") From 8f126463bc536dd4314573bf8107df1bac7b9097 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 29 Nov 2021 13:51:47 +0200 Subject: [PATCH 123/150] tox: update mysqlclient version --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 287bd4031..82db074f1 100644 --- a/tox.ini +++ b/tox.ini @@ -16,8 +16,8 @@ deps = dj31: Django>=3.1,<3.2 dj22: Django>=2.2,<2.3 - mysql_myisam: mysqlclient==1.4.2.post1 - mysql_innodb: mysqlclient==1.4.2.post1 + mysql_myisam: mysqlclient==2.1.0 + mysql_innodb: mysqlclient==2.1.0 !pypy3-postgres: psycopg2-binary pypy3-postgres: psycopg2cffi From bf06939095d4bb261c3469b770628e5a558e4740 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Mon, 29 Nov 2021 19:54:50 +0100 Subject: [PATCH 124/150] Add Django 4.0 support. --- .github/workflows/main.yml | 12 ++++++++++++ README.rst | 2 +- setup.cfg | 1 + tox.ini | 7 ++++--- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 60816b096..a59df2ca3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -55,6 +55,10 @@ jobs: python: 3.8 allow_failure: false + - name: py310-dj40-postgres-xdist-coverage + python: '3.10' + allow_failure: false + - name: py310-dj32-postgres-xdist-coverage python: '3.10' allow_failure: false @@ -67,6 +71,10 @@ jobs: python: 3.9 allow_failure: false + - name: py39-dj40-mysql_innodb-coverage + python: 3.9 + allow_failure: false + - name: py37-dj31-mysql_innodb-coverage python: 3.7 allow_failure: false @@ -87,6 +95,10 @@ jobs: python: 3.8 allow_failure: false + - name: py38-dj40-sqlite-xdist-coverage + python: 3.8 + allow_failure: false + - name: py39-djmain-sqlite-coverage python: 3.9 allow_failure: true diff --git a/README.rst b/README.rst index 261cd1336..09c4fd82d 100644 --- a/README.rst +++ b/README.rst @@ -32,7 +32,7 @@ pytest-django allows you to test your Django project/applications with the `_ * Version compatibility: - * Django: 2.2, 3.1, 3.2 and latest main branch (compatible at the time of + * Django: 2.2, 3.1, 3.2, 4.0 and latest main branch (compatible at the time of each release) * Python: CPython>=3.5 or PyPy 3 * pytest: >=5.4 diff --git a/setup.cfg b/setup.cfg index 0546efb94..bc670a468 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,7 @@ classifiers = Framework :: Django :: 2.2 Framework :: Django :: 3.1 Framework :: Django :: 3.2 + Framework :: Django :: 4.0 Intended Audience :: Developers License :: OSI Approved :: BSD License Operating System :: OS Independent diff --git a/tox.ini b/tox.ini index 82db074f1..aa72d5763 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,8 @@ [tox] envlist = - py310-dj{main,32}-postgres - py39-dj{main,32,31,22}-postgres - py38-dj{main,32,31,22}-postgres + py310-dj{main,40,32}-postgres + py39-dj{main,40,32,31,22}-postgres + py38-dj{main,40,32,31,22}-postgres py37-dj{32,31,22}-postgres py36-dj{32,31,22}-postgres py35-dj{22}-postgres @@ -12,6 +12,7 @@ envlist = extras = testing deps = djmain: https://github.com/django/django/archive/main.tar.gz + dj40: Django>=4.0a1,<4.1 dj32: Django>=3.2,<4.0 dj31: Django>=3.1,<3.2 dj22: Django>=2.2,<2.3 From 46c7d61da4a775ee4cd1e01b9b8cf4efa292c2a3 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Mon, 29 Nov 2021 19:56:43 +0100 Subject: [PATCH 125/150] Add myself to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index fbeb394b6..2272df0f5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -13,3 +13,4 @@ Rafal Stozek Donald Stufft Nicolas Delaby Daniel Hahler +Hasan Ramezani From 2bba6ebabcf9a7dd2232f828db50408db5d107fd Mon Sep 17 00:00:00 2001 From: Michael Howitz Date: Wed, 29 Sep 2021 11:30:38 +0200 Subject: [PATCH 126/150] Fix `LiveServer` for in-memory SQLite database `allow_thread_sharing` is no longer writeable since Django 2.2. --- AUTHORS | 1 + docs/changelog.rst | 10 ++++++++++ pytest_django/live_server_helper.py | 6 +++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 2272df0f5..2a64d0648 100644 --- a/AUTHORS +++ b/AUTHORS @@ -14,3 +14,4 @@ Donald Stufft Nicolas Delaby Daniel Hahler Hasan Ramezani +Michael Howitz diff --git a/docs/changelog.rst b/docs/changelog.rst index 5b04c3ba4..d8acf172b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,16 @@ Changelog ========= +unreleased +---------- + +Bugfixes +^^^^^^^^ + +* Fix :fixture:`live_server` when using an in-memory SQLite database on + Django >= 3.0. + + v4.4.0 (2021-06-06) ------------------- diff --git a/pytest_django/live_server_helper.py b/pytest_django/live_server_helper.py index 943198d7c..38fdbeeb2 100644 --- a/pytest_django/live_server_helper.py +++ b/pytest_django/live_server_helper.py @@ -24,7 +24,7 @@ def __init__(self, addr: str) -> None: and conn.settings_dict["NAME"] == ":memory:" ): # Explicitly enable thread-shareability for this connection - conn.allow_thread_sharing = True + conn.inc_thread_sharing() connections_override[conn.alias] = conn liveserver_kwargs["connections_override"] = connections_override @@ -60,8 +60,12 @@ def __init__(self, addr: str) -> None: def stop(self) -> None: """Stop the server""" + # Terminate the live server's thread. self.thread.terminate() self.thread.join() + # Restore shared connections' non-shareability. + for conn in self.thread.connections_override.values(): + conn.dec_thread_sharing() @property def url(self) -> str: From 774277b25b2334aac8cec7d39cd386df24a0501f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 1 Dec 2021 11:53:37 +0200 Subject: [PATCH 127/150] live_server_helper: remove redundant join() terminate() already does it. --- pytest_django/live_server_helper.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pytest_django/live_server_helper.py b/pytest_django/live_server_helper.py index 38fdbeeb2..f1f6b21ff 100644 --- a/pytest_django/live_server_helper.py +++ b/pytest_django/live_server_helper.py @@ -62,7 +62,6 @@ def stop(self) -> None: """Stop the server""" # Terminate the live server's thread. self.thread.terminate() - self.thread.join() # Restore shared connections' non-shareability. for conn in self.thread.connections_override.values(): conn.dec_thread_sharing() From f64b25291984e818bfbd86899d69adac3d2ca909 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 1 Dec 2021 12:04:32 +0200 Subject: [PATCH 128/150] live_server_helper: clean up on thread error --- pytest_django/live_server_helper.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pytest_django/live_server_helper.py b/pytest_django/live_server_helper.py index f1f6b21ff..1dbac6149 100644 --- a/pytest_django/live_server_helper.py +++ b/pytest_django/live_server_helper.py @@ -50,13 +50,17 @@ def __init__(self, addr: str) -> None: self._live_server_modified_settings = modify_settings( ALLOWED_HOSTS={"append": host} ) + # `_live_server_modified_settings` is enabled and disabled by + # `_live_server_helper`. self.thread.daemon = True self.thread.start() self.thread.is_ready.wait() if self.thread.error: - raise self.thread.error + error = self.thread.error + self.stop() + raise error def stop(self) -> None: """Stop the server""" From 904a99507bfd2d9570ce5d21deb006c4ae3f7ad3 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 1 Dec 2021 12:25:55 +0200 Subject: [PATCH 129/150] live_server_helper: improve sqlite in-memory check This will make it so it's covered by our tests. --- pytest_django/live_server_helper.py | 7 ++----- pytest_django_test/db_helpers.py | 6 ++++-- pytest_django_test/settings_sqlite.py | 6 +++--- pytest_django_test/settings_sqlite_file.py | 8 ++++++-- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/pytest_django/live_server_helper.py b/pytest_django/live_server_helper.py index 1dbac6149..0251d24c7 100644 --- a/pytest_django/live_server_helper.py +++ b/pytest_django/live_server_helper.py @@ -19,11 +19,8 @@ def __init__(self, addr: str) -> None: for conn in connections.all(): # If using in-memory sqlite databases, pass the connections to # the server thread. - if ( - conn.settings_dict["ENGINE"] == "django.db.backends.sqlite3" - and conn.settings_dict["NAME"] == ":memory:" - ): - # Explicitly enable thread-shareability for this connection + if conn.vendor == 'sqlite' and conn.is_in_memory_db(): + # Explicitly enable thread-shareability for this connection. conn.inc_thread_sharing() connections_override[conn.alias] = conn diff --git a/pytest_django_test/db_helpers.py b/pytest_django_test/db_helpers.py index 14ebe333a..d984b1d12 100644 --- a/pytest_django_test/db_helpers.py +++ b/pytest_django_test/db_helpers.py @@ -15,6 +15,8 @@ if _settings["ENGINE"] == "django.db.backends.sqlite3" and TEST_DB_NAME is None: TEST_DB_NAME = ":memory:" + SECOND_DB_NAME = ":memory:" + SECOND_TEST_DB_NAME = ":memory:" else: DB_NAME += "_inner" @@ -25,8 +27,8 @@ # An explicit test db name was given, is that as the base name TEST_DB_NAME = "{}_inner".format(TEST_DB_NAME) -SECOND_DB_NAME = DB_NAME + '_second' if DB_NAME is not None else None -SECOND_TEST_DB_NAME = TEST_DB_NAME + '_second' if DB_NAME is not None else None + SECOND_DB_NAME = DB_NAME + '_second' if DB_NAME is not None else None + SECOND_TEST_DB_NAME = TEST_DB_NAME + '_second' if DB_NAME is not None else None def get_db_engine(): diff --git a/pytest_django_test/settings_sqlite.py b/pytest_django_test/settings_sqlite.py index f58cc7d89..057b83449 100644 --- a/pytest_django_test/settings_sqlite.py +++ b/pytest_django_test/settings_sqlite.py @@ -4,17 +4,17 @@ DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", - "NAME": "/should_not_be_accessed", + "NAME": ":memory:", }, "replica": { "ENGINE": "django.db.backends.sqlite3", - "NAME": "/should_not_be_accessed", + "NAME": ":memory:", "TEST": { "MIRROR": "default", }, }, "second": { "ENGINE": "django.db.backends.sqlite3", - "NAME": "/should_not_be_accessed", + "NAME": ":memory:", }, } diff --git a/pytest_django_test/settings_sqlite_file.py b/pytest_django_test/settings_sqlite_file.py index 4f3404a09..206b658a2 100644 --- a/pytest_django_test/settings_sqlite_file.py +++ b/pytest_django_test/settings_sqlite_file.py @@ -15,7 +15,9 @@ "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": "/pytest_django_tests_default", - "TEST": {"NAME": _filename_default}, + "TEST": { + "NAME": _filename_default, + }, }, "replica": { "ENGINE": "django.db.backends.sqlite3", @@ -28,6 +30,8 @@ "second": { "ENGINE": "django.db.backends.sqlite3", "NAME": "/pytest_django_tests_second", - "TEST": {"NAME": _filename_second}, + "TEST": { + "NAME": _filename_second, + }, }, } From d6ea40f1da53fd2acb5dd124ba38f7d0bb5efc6e Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 28 Nov 2021 18:04:06 +0200 Subject: [PATCH 130/150] Add support for rollback emulation/serialized rollback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks to Aymeric Augustin, Daniel Hahler and Fábio C. Barrionuevo da Luz for previous work on this feature. Fix #329. Closes #353. Closes #721. Closes #919. Closes #956. --- docs/changelog.rst | 11 +++++- docs/helpers.rst | 45 +++++++++++++++++---- pytest_django/fixtures.py | 54 +++++++++++++++++++++---- pytest_django/plugin.py | 15 +++++-- tests/test_database.py | 83 ++++++++++++++++++++++++++++++++++++++- tests/test_db_setup.py | 5 +++ 6 files changed, 192 insertions(+), 21 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d8acf172b..2d800dbcc 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,11 +4,18 @@ Changelog unreleased ---------- +Improvements +^^^^^^^^^^^^ + +* Add support for :ref:`rollback emulation/serialized rollback + `. The :func:`pytest.mark.django_db` marker + has a new ``serialized_rollback`` option, and a + :fixture:`django_db_serialized_rollback` fixture is added. + Bugfixes ^^^^^^^^ -* Fix :fixture:`live_server` when using an in-memory SQLite database on - Django >= 3.0. +* Fix :fixture:`live_server` when using an in-memory SQLite database. v4.4.0 (2021-06-06) diff --git a/docs/helpers.rst b/docs/helpers.rst index 774237b39..1fd598693 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -73,15 +73,27 @@ dynamically in a hook or fixture. For details see :py:attr:`django.test.TransactionTestCase.databases` and :py:attr:`django.test.TestCase.databases`. + :type serialized_rollback: bool + :param serialized_rollback: + The ``serialized_rollback`` argument enables :ref:`rollback emulation + `. After a transactional test (or any test + using a database backend which doesn't support transactions) runs, the + database is flushed, destroying data created in data migrations. Setting + ``serialized_rollback=True`` tells Django to serialize the database content + during setup, and restore it during teardown. + + Note that this will slow down that test suite by approximately 3x. + .. note:: If you want access to the Django database inside a *fixture*, this marker may or may not help even if the function requesting your fixture has this marker - applied, depending on pytest's fixture execution order. To access the - database in a fixture, it is recommended that the fixture explicitly request - one of the :fixture:`db`, :fixture:`transactional_db` or - :fixture:`django_db_reset_sequences` fixtures. See below for a description of - them. + applied, depending on pytest's fixture execution order. To access the database + in a fixture, it is recommended that the fixture explicitly request one of the + :fixture:`db`, :fixture:`transactional_db`, + :fixture:`django_db_reset_sequences` or + :fixture:`django_db_serialized_rollback` fixtures. See below for a description + of them. .. note:: Automatic usage with ``django.test.TestCase``. @@ -331,6 +343,17 @@ fixtures which need database access themselves. A test function should normally use the :func:`pytest.mark.django_db` mark with ``transaction=True`` and ``reset_sequences=True``. +.. fixture:: django_db_serialized_rollback + +``django_db_serialized_rollback`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This fixture triggers :ref:`rollback emulation `. +This is only required for fixtures which need to enforce this behavior. A test +function should normally use :func:`pytest.mark.django_db` with +``serialized_rollback=True`` (and most likely also ``transaction=True``) to +request this behavior. + .. fixture:: live_server ``live_server`` @@ -342,6 +365,12 @@ or by requesting it's string value: ``str(live_server)``. You can also directly concatenate a string to form a URL: ``live_server + '/foo'``. +Since the live server and the tests run in different threads, they +cannot share a database transaction. For this reason, ``live_server`` +depends on the ``transactional_db`` fixture. If tests depend on data +created in data migrations, you should add the +``django_db_serialized_rollback`` fixture. + .. note:: Combining database access fixtures. When using multiple database fixtures together, only one of them is @@ -349,10 +378,10 @@ also directly concatenate a string to form a URL: ``live_server + * ``db`` * ``transactional_db`` - * ``django_db_reset_sequences`` - In addition, using ``live_server`` will also trigger transactional - database access, if not specified. + In addition, using ``live_server`` or ``django_db_reset_sequences`` will also + trigger transactional database access, and ``django_db_serialized_rollback`` + regular database access, if not specified. .. fixture:: settings diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 8c955858c..71e402a22 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -20,7 +20,8 @@ import django _DjangoDbDatabases = Optional[Union["Literal['__all__']", Iterable[str]]] - _DjangoDb = Tuple[bool, bool, _DjangoDbDatabases] + # transaction, reset_sequences, databases, serialized_rollback + _DjangoDb = Tuple[bool, bool, _DjangoDbDatabases, bool] __all__ = [ @@ -28,6 +29,7 @@ "db", "transactional_db", "django_db_reset_sequences", + "django_db_serialized_rollback", "admin_user", "django_user_model", "django_username_field", @@ -151,9 +153,19 @@ def _django_db_helper( marker = request.node.get_closest_marker("django_db") if marker: - transactional, reset_sequences, databases = validate_django_db(marker) + ( + transactional, + reset_sequences, + databases, + serialized_rollback, + ) = validate_django_db(marker) else: - transactional, reset_sequences, databases = False, False, None + ( + transactional, + reset_sequences, + databases, + serialized_rollback, + ) = False, False, None, False transactional = transactional or ( "transactional_db" in request.fixturenames @@ -162,6 +174,9 @@ def _django_db_helper( reset_sequences = reset_sequences or ( "django_db_reset_sequences" in request.fixturenames ) + serialized_rollback = serialized_rollback or ( + "django_db_serialized_rollback" in request.fixturenames + ) django_db_blocker.unblock() request.addfinalizer(django_db_blocker.restore) @@ -175,10 +190,12 @@ def _django_db_helper( test_case_class = django.test.TestCase _reset_sequences = reset_sequences + _serialized_rollback = serialized_rollback _databases = databases class PytestDjangoTestCase(test_case_class): # type: ignore[misc,valid-type] reset_sequences = _reset_sequences + serialized_rollback = _serialized_rollback if _databases is not None: databases = _databases @@ -196,18 +213,20 @@ def validate_django_db(marker) -> "_DjangoDb": """Validate the django_db marker. It checks the signature and creates the ``transaction``, - ``reset_sequences`` and ``databases`` attributes on the marker - which will have the correct values. + ``reset_sequences``, ``databases`` and ``serialized_rollback`` attributes on + the marker which will have the correct values. - A sequence reset is only allowed when combined with a transaction. + Sequence reset and serialized_rollback are only allowed when combined with + transaction. """ def apifun( transaction: bool = False, reset_sequences: bool = False, databases: "_DjangoDbDatabases" = None, + serialized_rollback: bool = False, ) -> "_DjangoDb": - return transaction, reset_sequences, databases + return transaction, reset_sequences, databases, serialized_rollback return apifun(*marker.args, **marker.kwargs) @@ -303,6 +322,27 @@ def django_db_reset_sequences( # is requested. +@pytest.fixture(scope="function") +def django_db_serialized_rollback( + _django_db_helper: None, + db: None, +) -> None: + """Require a test database with serialized rollbacks. + + This requests the ``db`` fixture, and additionally performs rollback + emulation - serializes the database contents during setup and restores + it during teardown. + + This fixture may be useful for transactional tests, so is usually combined + with ``transactional_db``, but can also be useful on databases which do not + support transactions. + + Note that this will slow down that test suite by approximately 3x. + """ + # The `_django_db_helper` fixture checks if `django_db_serialized_rollback` + # is requested. + + @pytest.fixture() def client() -> "django.test.client.Client": """A Django test client instance.""" diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 6dc812b21..845c4aecf 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -33,6 +33,7 @@ from .fixtures import django_db_modify_db_settings_tox_suffix # noqa from .fixtures import django_db_modify_db_settings_xdist_suffix # noqa from .fixtures import django_db_reset_sequences # noqa +from .fixtures import django_db_serialized_rollback # noqa from .fixtures import django_db_setup # noqa from .fixtures import django_db_use_migrations # noqa from .fixtures import django_user_model # noqa @@ -265,14 +266,17 @@ def pytest_load_initial_conftests( # Register the marks early_config.addinivalue_line( "markers", - "django_db(transaction=False, reset_sequences=False, databases=None): " + "django_db(transaction=False, reset_sequences=False, databases=None, " + "serialized_rollback=False): " "Mark the test as using the Django test database. " "The *transaction* argument allows you to use real transactions " "in the test like Django's TransactionTestCase. " "The *reset_sequences* argument resets database sequences before " "the test. " "The *databases* argument sets which database aliases the test " - "uses (by default, only 'default'). Use '__all__' for all databases.", + "uses (by default, only 'default'). Use '__all__' for all databases. " + "The *serialized_rollback* argument enables rollback emulation for " + "the test.", ) early_config.addinivalue_line( "markers", @@ -387,7 +391,12 @@ def get_order_number(test: pytest.Item) -> int: else: marker_db = test.get_closest_marker('django_db') if marker_db: - transaction, reset_sequences, databases = validate_django_db(marker_db) + ( + transaction, + reset_sequences, + databases, + serialized_rollback, + ) = validate_django_db(marker_db) uses_db = True transactional = transaction or reset_sequences else: diff --git a/tests/test_database.py b/tests/test_database.py index 9016240b9..df0d64709 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -48,7 +48,12 @@ def non_zero_sequences_counter(db: None) -> None: class TestDatabaseFixtures: """Tests for the different database fixtures.""" - @pytest.fixture(params=["db", "transactional_db", "django_db_reset_sequences"]) + @pytest.fixture(params=[ + "db", + "transactional_db", + "django_db_reset_sequences", + "django_db_serialized_rollback", + ]) def all_dbs(self, request) -> None: if request.param == "django_db_reset_sequences": return request.getfixturevalue("django_db_reset_sequences") @@ -56,6 +61,10 @@ def all_dbs(self, request) -> None: return request.getfixturevalue("transactional_db") elif request.param == "db": return request.getfixturevalue("db") + elif request.param == "django_db_serialized_rollback": + return request.getfixturevalue("django_db_serialized_rollback") + else: + assert False # pragma: no cover def test_access(self, all_dbs: None) -> None: Item.objects.create(name="spam") @@ -113,6 +122,51 @@ def test_django_db_reset_sequences_requested( ["*test_django_db_reset_sequences_requested PASSED*"] ) + def test_serialized_rollback(self, db: None, django_testdir) -> None: + django_testdir.create_app_file( + """ + from django.db import migrations + + def load_data(apps, schema_editor): + Item = apps.get_model("app", "Item") + Item.objects.create(name="loaded-in-migration") + + class Migration(migrations.Migration): + dependencies = [ + ("app", "0001_initial"), + ] + + operations = [ + migrations.RunPython(load_data), + ] + """, + "migrations/0002_data_migration.py", + ) + + django_testdir.create_test_module( + """ + import pytest + from .app.models import Item + + @pytest.mark.django_db(transaction=True, serialized_rollback=True) + def test_serialized_rollback_1(): + assert Item.objects.filter(name="loaded-in-migration").exists() + + @pytest.mark.django_db(transaction=True) + def test_serialized_rollback_2(django_db_serialized_rollback): + assert Item.objects.filter(name="loaded-in-migration").exists() + Item.objects.create(name="test2") + + @pytest.mark.django_db(transaction=True, serialized_rollback=True) + def test_serialized_rollback_3(): + assert Item.objects.filter(name="loaded-in-migration").exists() + assert not Item.objects.filter(name="test2").exists() + """ + ) + + result = django_testdir.runpytest_subprocess("-v") + assert result.ret == 0 + @pytest.fixture def mydb(self, all_dbs: None) -> None: # This fixture must be able to access the database @@ -160,6 +214,10 @@ def fixture_with_transdb(self, transactional_db: None) -> None: def fixture_with_reset_sequences(self, django_db_reset_sequences: None) -> None: Item.objects.create(name="spam") + @pytest.fixture + def fixture_with_serialized_rollback(self, django_db_serialized_rollback: None) -> None: + Item.objects.create(name="ham") + def test_trans(self, fixture_with_transdb: None) -> None: pass @@ -180,6 +238,16 @@ def test_reset_sequences( ) -> None: pass + # The test works when transactions are not supported, but it interacts + # badly with other tests. + @pytest.mark.skipif('not connection.features.supports_transactions') + def test_serialized_rollback( + self, + fixture_with_serialized_rollback: None, + fixture_with_db: None, + ) -> None: + pass + class TestDatabaseMarker: "Tests for the django_db marker." @@ -264,6 +332,19 @@ def test_all_databases(self, request) -> None: SecondItem.objects.count() SecondItem.objects.create(name="spam") + @pytest.mark.django_db + def test_serialized_rollback_disabled(self, request): + marker = request.node.get_closest_marker("django_db") + assert not marker.kwargs + + # The test works when transactions are not supported, but it interacts + # badly with other tests. + @pytest.mark.skipif('not connection.features.supports_transactions') + @pytest.mark.django_db(serialized_rollback=True) + def test_serialized_rollback_enabled(self, request): + marker = request.node.get_closest_marker("django_db") + assert marker.kwargs["serialized_rollback"] + def test_unittest_interaction(django_testdir) -> None: "Test that (non-Django) unittests cannot access the DB." diff --git a/tests/test_db_setup.py b/tests/test_db_setup.py index b529965d3..380c5662e 100644 --- a/tests/test_db_setup.py +++ b/tests/test_db_setup.py @@ -56,6 +56,10 @@ def test_run_second_reset_sequences_decorator(): def test_run_first_decorator(): pass + @pytest.mark.django_db(serialized_rollback=True) + def test_run_first_serialized_rollback_decorator(): + pass + class MyTestCase(TestCase): def test_run_last_test_case(self): pass @@ -77,6 +81,7 @@ def test_run_second_transaction_test_case(self): result.stdout.fnmatch_lines([ "*test_run_first_fixture*", "*test_run_first_decorator*", + "*test_run_first_serialized_rollback_decorator*", "*test_run_first_django_test_case*", "*test_run_second_decorator*", "*test_run_second_fixture*", From 154b5f3cf5262bdfb2e4cd8af925597039011ea7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 29 Nov 2021 21:10:09 +0200 Subject: [PATCH 131/150] Skip Django's `setUpTestData` mechanism in pytest-django tests --- pytest_django/fixtures.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 71e402a22..9cb732693 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -199,6 +199,32 @@ class PytestDjangoTestCase(test_case_class): # type: ignore[misc,valid-type] if _databases is not None: databases = _databases + # For non-transactional tests, skip executing `django.test.TestCase`'s + # `setUpClass`/`tearDownClass`, only execute the super class ones. + # + # `TestCase`'s class setup manages the `setUpTestData`/class-level + # transaction functionality. We don't use it; instead we (will) offer + # our own alternatives. So it only adds overhead, and does some things + # which conflict with our (planned) functionality, particularly, it + # closes all database connections in `tearDownClass` which inhibits + # wrapping tests in higher-scoped transactions. + # + # It's possible a new version of Django will add some unrelated + # functionality to these methods, in which case skipping them completely + # would not be desirable. Let's cross that bridge when we get there... + if not transactional: + @classmethod + def setUpClass(cls) -> None: + super(django.test.TestCase, cls).setUpClass() + if (3, 2) <= VERSION < (4, 1): + django.db.transaction.Atomic._ensure_durability = False + + @classmethod + def tearDownClass(cls) -> None: + if (3, 2) <= VERSION < (4, 1): + django.db.transaction.Atomic._ensure_durability = True + super(django.test.TestCase, cls).tearDownClass() + PytestDjangoTestCase.setUpClass() if VERSION >= (4, 0): request.addfinalizer(PytestDjangoTestCase.doClassCleanups) From a3b327d06cb0b6f14b0d5f8ef00c70f252643cc3 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 1 Dec 2021 16:24:04 +0200 Subject: [PATCH 132/150] Update AUTHORS with current status --- AUTHORS | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 2a64d0648..3f9b7ea65 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,6 +1,11 @@ Ben Firshman created the original version of pytest-django. -This fork is currently maintained by Andreas Pelme . +This project is currently maintained by Ran Benita . + +Previous maintainers are: + +Andreas Pelme +Daniel Hahler These people have provided bug fixes, new features, improved the documentation or just made pytest-django more awesome: @@ -12,6 +17,5 @@ Floris Bruynooghe Rafal Stozek Donald Stufft Nicolas Delaby -Daniel Hahler Hasan Ramezani Michael Howitz From a920a7ff4973fd179b5ee0e1acb64ec3e94a3f4c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 1 Dec 2021 16:21:49 +0200 Subject: [PATCH 133/150] Release 4.5.0 --- docs/changelog.rst | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 2d800dbcc..8f4e9c8bf 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,8 +1,8 @@ Changelog ========= -unreleased ----------- +v4.5.0 (2021-12-01) +------------------- Improvements ^^^^^^^^^^^^ @@ -12,11 +12,27 @@ Improvements has a new ``serialized_rollback`` option, and a :fixture:`django_db_serialized_rollback` fixture is added. +* Official Python 3.10 support. + +* Official Django 4.0 support (tested against 4.0rc1 at the time of release). + +* Drop official Django 3.0 support. Django 2.2 is still supported, and 3.0 + will likely keep working until 2.2 is dropped, but it's not tested. + +* Added pyproject.toml file. + +* Skip Django's `setUpTestData` mechanism in pytest-django tests. It is not + used for those, and interferes with some planned features. Note that this + does not affect ``setUpTestData`` in unittest tests (test classes which + inherit from Django's `TestCase`). + Bugfixes ^^^^^^^^ * Fix :fixture:`live_server` when using an in-memory SQLite database. +* Fix typing of ``assertTemplateUsed`` and ``assertTemplateNotUsed``. + v4.4.0 (2021-06-06) ------------------- From 54f4626209dc21d988cd84933038c3436e0bdb81 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 2 Dec 2021 09:31:08 +0200 Subject: [PATCH 134/150] Fix transactional tests in classes not sorted correctly after regular tests Regression in 4.5.0. Fix #975. --- pytest_django/plugin.py | 15 ++++--------- tests/test_db_setup.py | 47 +++++++++++++++++++++++++---------------- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 845c4aecf..f8ce8c2ea 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -377,17 +377,10 @@ def pytest_collection_modifyitems(items: List[pytest.Item]) -> None: def get_order_number(test: pytest.Item) -> int: test_cls = getattr(test, "cls", None) - if test_cls: - # Beware, TestCase is a subclass of TransactionTestCase - if issubclass(test_cls, TestCase): - uses_db = True - transactional = False - elif issubclass(test_cls, TransactionTestCase): - uses_db = True - transactional = True - else: - uses_db = False - transactional = False + if test_cls and issubclass(test_cls, TransactionTestCase): + # Note, TestCase is a subclass of TransactionTestCase. + uses_db = True + transactional = not issubclass(test_cls, TestCase) else: marker_db = test.get_closest_marker('django_db') if marker_db: diff --git a/tests/test_db_setup.py b/tests/test_db_setup.py index 380c5662e..8f10a6804 100644 --- a/tests/test_db_setup.py +++ b/tests/test_db_setup.py @@ -29,9 +29,11 @@ def test_db_order(django_testdir) -> None: """Test order in which tests are being executed.""" django_testdir.create_test_module(''' - from unittest import TestCase import pytest - from django.test import SimpleTestCase, TestCase as DjangoTestCase, TransactionTestCase + from unittest import TestCase + from django.test import SimpleTestCase + from django.test import TestCase as DjangoTestCase + from django.test import TransactionTestCase from .app.models import Item @@ -45,13 +47,32 @@ def test_run_second_fixture(transactional_db): def test_run_second_reset_sequences_fixture(django_db_reset_sequences): pass + class MyTransactionTestCase(TransactionTestCase): + def test_run_second_transaction_test_case(self): + pass + def test_run_first_fixture(db): pass + class TestClass: + def test_run_second_fixture_class(self, transactional_db): + pass + + def test_run_first_fixture_class(self, db): + pass + @pytest.mark.django_db(reset_sequences=True) def test_run_second_reset_sequences_decorator(): pass + class MyDjangoTestCase(DjangoTestCase): + def test_run_first_django_test_case(self): + pass + + class MySimpleTestCase(SimpleTestCase): + def test_run_last_simple_test_case(self): + pass + @pytest.mark.django_db def test_run_first_decorator(): pass @@ -63,34 +84,24 @@ def test_run_first_serialized_rollback_decorator(): class MyTestCase(TestCase): def test_run_last_test_case(self): pass - - class MySimpleTestCase(SimpleTestCase): - def test_run_last_simple_test_case(self): - pass - - class MyDjangoTestCase(DjangoTestCase): - def test_run_first_django_test_case(self): - pass - - class MyTransactionTestCase(TransactionTestCase): - def test_run_second_transaction_test_case(self): - pass ''') result = django_testdir.runpytest_subprocess('-q', '--collect-only') assert result.ret == 0 result.stdout.fnmatch_lines([ "*test_run_first_fixture*", + "*test_run_first_fixture_class*", + "*test_run_first_django_test_case*", "*test_run_first_decorator*", "*test_run_first_serialized_rollback_decorator*", - "*test_run_first_django_test_case*", "*test_run_second_decorator*", "*test_run_second_fixture*", "*test_run_second_reset_sequences_fixture*", - "*test_run_second_reset_sequences_decorator*", "*test_run_second_transaction_test_case*", - "*test_run_last_test_case*", + "*test_run_second_fixture_class*", + "*test_run_second_reset_sequences_decorator*", "*test_run_last_simple_test_case*", - ]) + "*test_run_last_test_case*", + ], consecutive=True) def test_db_reuse(django_testdir) -> None: From c48b99e378e199acecf4dc23ed9a527bb58a38da Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 2 Dec 2021 09:33:11 +0200 Subject: [PATCH 135/150] Fix string quote style --- pytest_django/asserts.py | 10 +++++----- pytest_django/live_server_helper.py | 2 +- pytest_django/plugin.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pytest_django/asserts.py b/pytest_django/asserts.py index ff2102adb..a589abd2d 100644 --- a/pytest_django/asserts.py +++ b/pytest_django/asserts.py @@ -12,7 +12,7 @@ TYPE_CHECKING = False -test_case = TestCase('run') +test_case = TestCase("run") def _wrapper(name: str): @@ -28,10 +28,10 @@ def assertion_func(*args, **kwargs): __all__ = [] assertions_names = set() # type: Set[str] assertions_names.update( - {attr for attr in vars(TestCase) if attr.startswith('assert')}, - {attr for attr in vars(SimpleTestCase) if attr.startswith('assert')}, - {attr for attr in vars(LiveServerTestCase) if attr.startswith('assert')}, - {attr for attr in vars(TransactionTestCase) if attr.startswith('assert')}, + {attr for attr in vars(TestCase) if attr.startswith("assert")}, + {attr for attr in vars(SimpleTestCase) if attr.startswith("assert")}, + {attr for attr in vars(LiveServerTestCase) if attr.startswith("assert")}, + {attr for attr in vars(TransactionTestCase) if attr.startswith("assert")}, ) for assert_func in assertions_names: diff --git a/pytest_django/live_server_helper.py b/pytest_django/live_server_helper.py index 0251d24c7..72ade43a8 100644 --- a/pytest_django/live_server_helper.py +++ b/pytest_django/live_server_helper.py @@ -19,7 +19,7 @@ def __init__(self, addr: str) -> None: for conn in connections.all(): # If using in-memory sqlite databases, pass the connections to # the server thread. - if conn.vendor == 'sqlite' and conn.is_in_memory_db(): + if conn.vendor == "sqlite" and conn.is_in_memory_db(): # Explicitly enable thread-shareability for this connection. conn.inc_thread_sharing() connections_override[conn.alias] = conn diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index f8ce8c2ea..aba46efd2 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -382,7 +382,7 @@ def get_order_number(test: pytest.Item) -> int: uses_db = True transactional = not issubclass(test_cls, TestCase) else: - marker_db = test.get_closest_marker('django_db') + marker_db = test.get_closest_marker("django_db") if marker_db: ( transaction, @@ -395,7 +395,7 @@ def get_order_number(test: pytest.Item) -> int: else: uses_db = False transactional = False - fixtures = getattr(test, 'fixturenames', []) + fixtures = getattr(test, "fixturenames", []) transactional = transactional or "transactional_db" in fixtures uses_db = uses_db or "db" in fixtures From 8b78acac9e1645723106a8dc656c6fe11f3e0614 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 2 Dec 2021 09:54:35 +0200 Subject: [PATCH 136/150] Release 4.5.1 --- docs/changelog.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8f4e9c8bf..ee4a06d50 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,17 @@ Changelog ========= +v4.5.1 (2021-12-02) +------------------- + +Bugfixes +^^^^^^^^ + +* Fix regression in v4.5.0 - database tests inside (non-unittest) classes were + not ordered correctly to run before non-database tests, same for transactional + tests before non-transactional tests. + + v4.5.0 (2021-12-01) ------------------- From 19f277b497507b756128cfed513a08552d248049 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 2 Dec 2021 10:11:31 +0200 Subject: [PATCH 137/150] ci: don't require tests before deploy Usually the tests have already been run by the commit push so this just slows things down. --- .github/workflows/main.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a59df2ca3..3b57573a6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -128,7 +128,6 @@ jobs: deploy: if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pytest-django' runs-on: ubuntu-20.04 - needs: [test] steps: - uses: actions/checkout@v2 From 0661d343f48eb5d5bcea84373ab7651c3b503cfd Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 2 Dec 2021 10:18:10 +0200 Subject: [PATCH 138/150] ci: some tweaks --- .github/workflows/main.yml | 27 ++++++++++++++++++++++----- Makefile | 5 +---- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3b57573a6..8b75981e1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,12 +10,23 @@ on: branches: - master +env: + PYTEST_ADDOPTS: "--color=yes" + +# Set permissions at the job level. +permissions: {} + jobs: test: runs-on: ubuntu-20.04 continue-on-error: ${{ matrix.allow_failure }} + timeout-minutes: 15 + permissions: + contents: read steps: - uses: actions/checkout@v2 + with: + persist-credentials: false - uses: actions/setup-python@v2 with: @@ -37,15 +48,17 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install tox==3.20.0 + pip install tox==3.24.4 - name: Run tox run: tox -e ${{ matrix.name }} - name: Report coverage if: contains(matrix.name, 'coverage') - run: | - bash <(curl -s https://codecov.io/bash) -Z -X gcov -X xcode -X gcovout + uses: codecov/codecov-action@v2 + with: + fail_ci_if_error: true + files: ./coverage.xml strategy: fail-fast: false @@ -128,11 +141,15 @@ jobs: deploy: if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pytest-django' runs-on: ubuntu-20.04 + timeout-minutes: 15 + permissions: + contents: read steps: - uses: actions/checkout@v2 with: fetch-depth: 0 + persist-credentials: false - uses: actions/setup-python@v2 with: @@ -141,10 +158,10 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install --upgrade wheel setuptools tox + pip install --upgrade build tox - name: Build package - run: python setup.py sdist bdist_wheel + run: python -m build - name: Publish package uses: pypa/gh-action-pypi-publish@v1.4.1 diff --git a/Makefile b/Makefile index ff84d7df5..ba5e3f500 100644 --- a/Makefile +++ b/Makefile @@ -4,11 +4,8 @@ VENV:=build/venv export DJANGO_SETTINGS_MODULE?=pytest_django_test.settings_sqlite_file -testenv: $(VENV)/bin/pytest - test: $(VENV)/bin/pytest - $(VENV)/bin/pip install -e . - $(VENV)/bin/py.test + $(VENV)/bin/pytest $(VENV)/bin/python $(VENV)/bin/pip: virtualenv $(VENV) From b3b679f2cab9dad70e318f252751ff7659b951d1 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 5 Dec 2021 11:55:35 +0200 Subject: [PATCH 139/150] ci: remove unused dep --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8b75981e1..e9c21b228 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -158,7 +158,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install --upgrade build tox + pip install --upgrade build - name: Build package run: python -m build From b97a8b17cf9fb02e27465910ec57cd692378b8a4 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 7 Dec 2021 16:10:16 +0200 Subject: [PATCH 140/150] tox: test django 4.0 final --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index aa72d5763..e5f3bb62e 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ envlist = extras = testing deps = djmain: https://github.com/django/django/archive/main.tar.gz - dj40: Django>=4.0a1,<4.1 + dj40: Django>=4.0,<4.1 dj32: Django>=3.2,<4.0 dj31: Django>=3.1,<3.2 dj22: Django>=2.2,<2.3 From 3bc9065c083e6ca87aa909c14c58a8de268f62ea Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 7 Dec 2021 16:06:18 +0200 Subject: [PATCH 141/150] Make `pytest.mark.django_db(reset_sequences=True)` imply `transaction=True` again Regressed in `4.5.0`. Though it would have been better if it hadn't, changing it now is a breaking change so needs to be fixed. --- docs/changelog.rst | 10 ++++++++++ pytest_django/fixtures.py | 2 +- tests/test_database.py | 7 ++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index ee4a06d50..d120e7cc6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,16 @@ Changelog ========= +v4.5.2 (2021-12-07) +------------------- + +Bugfixes +^^^^^^^^ + +* Fix regression in v4.5.0 - ``pytest.mark.django_db(reset_sequence=True)`` now + implies ``transaction=True`` again. + + v4.5.1 (2021-12-02) ------------------- diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 9cb732693..36020dcc4 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -167,7 +167,7 @@ def _django_db_helper( serialized_rollback, ) = False, False, None, False - transactional = transactional or ( + transactional = transactional or reset_sequences or ( "transactional_db" in request.fixturenames or "live_server" in request.fixturenames ) diff --git a/tests/test_database.py b/tests/test_database.py index df0d64709..510f4bffb 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -287,11 +287,16 @@ def test_reset_sequences_disabled(self, request) -> None: marker = request.node.get_closest_marker("django_db") assert not marker.kwargs - @pytest.mark.django_db(transaction=True, reset_sequences=True) + @pytest.mark.django_db(reset_sequences=True) def test_reset_sequences_enabled(self, request) -> None: marker = request.node.get_closest_marker("django_db") assert marker.kwargs["reset_sequences"] + @pytest.mark.django_db(transaction=True, reset_sequences=True) + def test_transaction_reset_sequences_enabled(self, request) -> None: + marker = request.node.get_closest_marker("django_db") + assert marker.kwargs["reset_sequences"] + @pytest.mark.django_db(databases=['default', 'replica', 'second']) def test_databases(self, request) -> None: marker = request.node.get_closest_marker("django_db") From 27d65607d82f6915bbc56f73779eab013f596708 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 7 Dec 2021 16:25:24 +0200 Subject: [PATCH 142/150] Release 4.5.2 From d0f53b86adf8961d25a1bef44bec7e0e689e01fd Mon Sep 17 00:00:00 2001 From: Jero Bado Date: Wed, 15 Dec 2021 23:40:19 +0800 Subject: [PATCH 143/150] Fix typo --- docs/managing_python_path.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/managing_python_path.rst b/docs/managing_python_path.rst index 083e4e364..6556c871f 100644 --- a/docs/managing_python_path.rst +++ b/docs/managing_python_path.rst @@ -60,7 +60,7 @@ This ``setup.py`` file is not sufficient to distribute your package to PyPI or more general packaging, but it should help you get started. Please refer to the `Python Packaging User Guide `_ -for more information on packaging Python applications.` +for more information on packaging Python applications. To install the project afterwards:: From b62715bde3f610855f165214ce77fd720dd8750c Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Thu, 16 Dec 2021 16:38:34 +0100 Subject: [PATCH 144/150] Drop support for Django 3.1 --- .github/workflows/main.yml | 16 ---------------- README.rst | 2 +- setup.cfg | 1 - tox.ini | 9 ++++----- 4 files changed, 5 insertions(+), 23 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e9c21b228..719e408f6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -80,18 +80,10 @@ jobs: python: 3.9 allow_failure: false - - name: py39-dj31-postgres-xdist-coverage - python: 3.9 - allow_failure: false - - name: py39-dj40-mysql_innodb-coverage python: 3.9 allow_failure: false - - name: py37-dj31-mysql_innodb-coverage - python: 3.7 - allow_failure: false - - name: py36-dj22-sqlite-xdist-coverage python: 3.6 allow_failure: false @@ -104,10 +96,6 @@ jobs: python: 3.8 allow_failure: false - - name: py38-dj31-sqlite-xdist-coverage - python: 3.8 - allow_failure: false - - name: py38-dj40-sqlite-xdist-coverage python: 3.8 allow_failure: false @@ -125,10 +113,6 @@ jobs: python: 3.5 allow_failure: false - - name: py36-dj31-mysql_myisam-coverage - python: 3.6 - allow_failure: false - - name: py36-dj32-mysql_myisam-coverage python: 3.6 allow_failure: false diff --git a/README.rst b/README.rst index 09c4fd82d..20be4f961 100644 --- a/README.rst +++ b/README.rst @@ -32,7 +32,7 @@ pytest-django allows you to test your Django project/applications with the `_ * Version compatibility: - * Django: 2.2, 3.1, 3.2, 4.0 and latest main branch (compatible at the time of + * Django: 2.2, 3.2, 4.0 and latest main branch (compatible at the time of each release) * Python: CPython>=3.5 or PyPy 3 * pytest: >=5.4 diff --git a/setup.cfg b/setup.cfg index bc670a468..259fb749d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,6 @@ classifiers = Development Status :: 5 - Production/Stable Framework :: Django Framework :: Django :: 2.2 - Framework :: Django :: 3.1 Framework :: Django :: 3.2 Framework :: Django :: 4.0 Intended Audience :: Developers diff --git a/tox.ini b/tox.ini index e5f3bb62e..4f2ccf4a2 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,10 @@ [tox] envlist = py310-dj{main,40,32}-postgres - py39-dj{main,40,32,31,22}-postgres - py38-dj{main,40,32,31,22}-postgres - py37-dj{32,31,22}-postgres - py36-dj{32,31,22}-postgres + py39-dj{main,40,32,22}-postgres + py38-dj{main,40,32,22}-postgres + py37-dj{32,22}-postgres + py36-dj{32,22}-postgres py35-dj{22}-postgres linting @@ -14,7 +14,6 @@ deps = djmain: https://github.com/django/django/archive/main.tar.gz dj40: Django>=4.0,<4.1 dj32: Django>=3.2,<4.0 - dj31: Django>=3.1,<3.2 dj22: Django>=2.2,<2.3 mysql_myisam: mysqlclient==2.1.0 From 19a2886029d8ad6b31a2b26c80cfd50731f1df54 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Thu, 16 Dec 2021 16:40:58 +0100 Subject: [PATCH 145/150] Update mypy to 0.920 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 4f2ccf4a2..062449358 100644 --- a/tox.ini +++ b/tox.ini @@ -53,7 +53,7 @@ commands = extras = deps = flake8 - mypy==0.910 + mypy==0.920 isort commands = flake8 --version From ed77ef2916013eaa7924ef6d21f7c8266ae4f670 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Mon, 27 Dec 2021 23:07:17 +0100 Subject: [PATCH 146/150] Update mypy to 0.930 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 062449358..af237fb3f 100644 --- a/tox.ini +++ b/tox.ini @@ -53,7 +53,7 @@ commands = extras = deps = flake8 - mypy==0.920 + mypy==0.930 isort commands = flake8 --version From 14b993d1cb291bfcf05f35f57db55c0419886534 Mon Sep 17 00:00:00 2001 From: Stanislav Levin Date: Mon, 28 Feb 2022 13:12:37 +0300 Subject: [PATCH 147/150] tests: Sync expected stream for Pytest's version https://docs.pytest.org/en/7.0.x/changelog.html#breaking-changes: > [pytest#8246](https://github.com/pytest-dev/pytest/issues/8246): --version now writes version information to stdout rather than stderr. Fixes: https://github.com/pytest-dev/pytest-django/issues/995 Signed-off-by: Stanislav Levin --- tests/test_manage_py_scan.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_manage_py_scan.py b/tests/test_manage_py_scan.py index 395445897..490882b05 100644 --- a/tests/test_manage_py_scan.py +++ b/tests/test_manage_py_scan.py @@ -118,7 +118,12 @@ def test_django_project_found_invalid_settings_version(django_testdir, monkeypat result = django_testdir.runpytest_subprocess("django_project_root", "--version", "--version") assert result.ret == 0 - result.stderr.fnmatch_lines(["*This is pytest version*"]) + if hasattr(pytest, "version_tuple") and pytest.version_tuple >= (7, 0): + version_out = result.stdout + else: + version_out = result.stderr + + version_out.fnmatch_lines(["*This is pytest version*"]) result = django_testdir.runpytest_subprocess("django_project_root", "--help") assert result.ret == 0 From 8f4de08bdff4c614e6231b87a883ae5d960dc496 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Mon, 28 Feb 2022 13:09:52 +0000 Subject: [PATCH 148/150] docs: configuration for the src layout (#989) * docs: configuration for the src layout Expand the documentation to explain how to configure the pythonpath for projects using the src layout. Fix #738 * Fix incorrect formatting Co-authored-by: Hasan Ramezani * Prefer the pytest 7 way Co-authored-by: Hasan Ramezani Co-authored-by: Ran Benita --- docs/managing_python_path.rst | 40 ++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/docs/managing_python_path.rst b/docs/managing_python_path.rst index 6556c871f..94af75d0b 100644 --- a/docs/managing_python_path.rst +++ b/docs/managing_python_path.rst @@ -75,9 +75,39 @@ add this directly to your project's requirements.txt file like this:: pytest-django -Using pytest-pythonpath -~~~~~~~~~~~~~~~~~~~~~~~ +Using pytest's ``pythonpath`` option +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can explicitly add paths to the Python search path using pytest's +:pytest-confval:`pythonpath` option. +This option is available since pytest 7; for older versions you can use the +`pytest-pythonpath `_ plugin. + +Example: project with src layout +```````````````````````````````` + +For a Django package using the ``src`` layout, with test settings located in a +``tests`` package at the top level:: + + myproj + ├── pytest.ini + ├── src + │ └── myproj + │ ├── __init__.py + │ └── main.py + └── tests + ├── testapp + | ├── __init__.py + | └── apps.py + ├── __init__.py + ├── settings.py + └── test_main.py + +You'll need to specify both the top level directory and ``src`` for things to work:: -You can also use the `pytest-pythonpath -`_ plugin to explicitly add paths to -the Python path. + [pytest] + DJANGO_SETTINGS_MODULE = tests.settings + pythonpath = . src + +If you don't specify ``.``, the settings module won't be found and +you'll get an import error: ``ImportError: No module named 'tests'``. From 24bb5545676061fef1cf07c0fa54127583ce3a8b Mon Sep 17 00:00:00 2001 From: Farooq Azam Date: Mon, 28 Feb 2022 23:14:22 +0500 Subject: [PATCH 149/150] Fix byte and string comparison `response.content` is a `bytes` object and doing a string comparison throws an error --- docs/helpers.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/helpers.rst b/docs/helpers.rst index 1fd598693..07061ea46 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -119,7 +119,7 @@ dynamically in a hook or fixture. @pytest.mark.urls('myapp.test_urls') def test_something(client): - assert 'Success!' in client.get('/some_url_defined_in_test_urls/').content + assert b'Success!' in client.get('/some_url_defined_in_test_urls/').content ``pytest.mark.ignore_template_errors`` - ignore invalid template variables From f27c6aa1d44beb46fefa4718fefcf541478643d7 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Mon, 14 Mar 2022 17:07:29 +0100 Subject: [PATCH 150/150] Update mypy to 0.940 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index af237fb3f..923556471 100644 --- a/tox.ini +++ b/tox.ini @@ -53,7 +53,7 @@ commands = extras = deps = flake8 - mypy==0.930 + mypy==0.940 isort commands = flake8 --version