From 292088c0ad19e54a9ccb62d531f14e9d10a6b597 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 14 Apr 2025 20:54:26 +0300 Subject: [PATCH 01/34] Remove experimental label from multi-db support It works, is in use, and there are no known issues, so let's make it stable. --- docs/changelog.rst | 8 ++++++++ docs/database.rst | 20 +++++++------------- docs/helpers.rst | 8 +------- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 16d40cb6..d5cd706c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,14 @@ Changelog ========= +v4.12.0 (Not released yet) +-------------------------- + +Improvements +^^^^^^^^^^^^ + +* The :ref:`multiple databases ` support added in v4.3.0 is no longer considered experimental. + v4.11.1 (2025-04-03) -------------------- diff --git a/docs/database.rst b/docs/database.rst index c4410a6a..fcdd219a 100644 --- a/docs/database.rst +++ b/docs/database.rst @@ -37,8 +37,8 @@ documentation ` for detail:: assert me.is_superuser -By default ``pytest-django`` will set up the Django databases the -first time a test needs them. Once setup, the database is cached to be +By default ``pytest-django`` will set up Django databases the +first time a test needs them. Once setup, a 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 @@ -67,22 +67,16 @@ Tests requiring multiple databases .. versionadded:: 4.3 -.. 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, using the ``databases`` argument to the -:func:`django_db ` mark:: +``pytest-django`` has support for multi-database configurations using the +``databases`` argument to the :func:`django_db ` mark:: @pytest.mark.django_db(databases=['default', 'other']) def test_spam(): assert MyModel.objects.using('other').count() == 0 +If you don't specify ``databases``, only the default database is requested. +To request all databases, you may use the shortcut ``'__all__'``. + For details see :attr:`django.test.TransactionTestCase.databases` and :attr:`django.test.TestCase.databases`. diff --git a/docs/helpers.rst b/docs/helpers.rst index c9e189dd..a1a6a59a 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -59,16 +59,10 @@ dynamically in a hook or fixture. :type databases: 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 + only the ``default`` database. The special value ``"__all__"`` may be used to specify all configured databases. For details see :attr:`django.test.TransactionTestCase.databases` and :attr:`django.test.TestCase.databases`. From 0494481dca2885855f3484726e76c81199accdf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miquel=20Torres=20Barcel=C3=B3?= Date: Mon, 26 May 2025 23:05:32 +0200 Subject: [PATCH 02/34] Add type definition for assertNotInHTML (#1200) --- pytest_django/asserts.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pytest_django/asserts.py b/pytest_django/asserts.py index 14741066..e64cebc1 100644 --- a/pytest_django/asserts.py +++ b/pytest_django/asserts.py @@ -160,6 +160,13 @@ def assertInHTML( msg_prefix: str = ..., ) -> None: ... + # Added in Django 5.1. + def assertNotInHTML( + needle: str, + haystack: str, + msg_prefix: str = ..., + ) -> None: ... + def assertJSONEqual( raw: str, expected_data: Any, From 7c99f33794113446ce865cbab825adb6ae7aee78 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Mon, 26 May 2025 17:35:07 -0400 Subject: [PATCH 03/34] Remove Python 3.8 & Django 5.0 (#1202) --- .github/workflows/main.yml | 14 +++++--------- README.rst | 4 ++-- docs/contributing.rst | 6 +++--- pyproject.toml | 4 +--- pytest_django/asserts.py | 3 ++- pytest_django/fixtures.py | 28 +++++++--------------------- pytest_django/plugin.py | 10 ++++++---- pytest_django/runner.py | 3 ++- pytest_django_test/db_helpers.py | 2 +- tests/test_database.py | 2 +- tests/test_fixtures.py | 2 +- tests/test_runner.py | 2 +- tox.ini | 7 +++---- 13 files changed, 35 insertions(+), 52 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d56f905e..6556866f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -71,6 +71,11 @@ jobs: python: '3.13' allow_failure: false + # Explicitly test min pytest. + - name: py313-dj52-sqlite-pytestmin-coverage + python: '3.13' + allow_failure: false + - name: py313-dj52-postgres-xdist-coverage python: '3.13' allow_failure: false @@ -131,15 +136,6 @@ jobs: python: '3.11' allow_failure: false - - name: py38-dj42-sqlite-xdist-coverage - python: '3.8' - allow_failure: false - - # Explicitly test min pytest. - - name: py38-dj42-sqlite-pytestmin-coverage - python: '3.8' - allow_failure: false - # pypy3: not included with coverage reports (much slower then). - name: pypy3-dj42-postgres python: 'pypy3.9' diff --git a/README.rst b/README.rst index 90a9fb8d..87291333 100644 --- a/README.rst +++ b/README.rst @@ -32,9 +32,9 @@ pytest-django allows you to test your Django project/applications with the `_ * Version compatibility: - * Django: 4.2, 5.0, 5.1, 5.2 and latest main branch (compatible at the time + * Django: 4.2, 5.1, 5.2 and latest main branch (compatible at the time of each release) - * Python: CPython>=3.8 or PyPy 3 + * Python: CPython>=3.9 or PyPy 3 * pytest: >=7.0 For compatibility with older versions, use previous pytest-django releases. diff --git a/docs/contributing.rst b/docs/contributing.rst index d5003fc7..897d4ae0 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -140,10 +140,10 @@ writing), running them all will take a long time. All valid configurations can be found in `tox.ini`. To test against a few of them, invoke tox with the `-e` flag:: - $ tox -e py38-dj32-postgres,py310-dj41-mysql + $ tox -e py39-dj42-postgres,py310-dj52-mysql -This will run the tests on Python 3.8/Django 3.2/PostgeSQL and Python -3.10/Django 4.1/MySQL. +This will run the tests on Python 3.9/Django 4.2/PostgeSQL and Python +3.10/Django 5.2/MySQL. Measuring test coverage diff --git a/pyproject.toml b/pyproject.toml index 3fb403da..61f2c006 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" name = "pytest-django" description = "A Django plugin for pytest." readme = "README.rst" -requires-python = ">=3.8" +requires-python = ">=3.9" dynamic = ["version"] authors = [ { name = "Andreas Pelme", email = "andreas@pelme.se" }, @@ -22,14 +22,12 @@ classifiers = [ "Development Status :: 5 - Production/Stable", "Framework :: Django", "Framework :: Django :: 4.2", - "Framework :: Django :: 5.0", "Framework :: Django :: 5.1", "Framework :: Django :: 5.2", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", diff --git a/pytest_django/asserts.py b/pytest_django/asserts.py index e64cebc1..f4e71dab 100644 --- a/pytest_django/asserts.py +++ b/pytest_django/asserts.py @@ -4,8 +4,9 @@ from __future__ import annotations +from collections.abc import Sequence from functools import wraps -from typing import TYPE_CHECKING, Any, Callable, Sequence +from typing import TYPE_CHECKING, Any, Callable from django import VERSION from django.test import LiveServerTestCase, SimpleTestCase, TestCase, TransactionTestCase diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 6dc05fdb..c1bf9ca1 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -3,24 +3,10 @@ from __future__ import annotations import os -from contextlib import contextmanager +from collections.abc import Generator, Iterable, Sequence +from contextlib import AbstractContextManager, contextmanager from functools import partial -from typing import ( - TYPE_CHECKING, - AbstractSet, - Any, - Callable, - ContextManager, - Generator, - Iterable, - List, - Literal, - Optional, - Protocol, - Sequence, - Tuple, - Union, -) +from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, Protocol, Union import pytest @@ -37,9 +23,9 @@ _DjangoDbDatabases = Optional[Union[Literal["__all__"], Iterable[str]]] -_DjangoDbAvailableApps = Optional[List[str]] +_DjangoDbAvailableApps = Optional[list[str]] # transaction, reset_sequences, databases, serialized_rollback, available_apps -_DjangoDb = Tuple[bool, bool, _DjangoDbDatabases, bool, _DjangoDbAvailableApps] +_DjangoDb = tuple[bool, bool, _DjangoDbDatabases, bool, _DjangoDbAvailableApps] __all__ = [ @@ -157,7 +143,7 @@ def _get_databases_for_test(test: pytest.Item) -> tuple[Iterable[str], bool]: def _get_databases_for_setup( items: Sequence[pytest.Item], -) -> tuple[AbstractSet[str], AbstractSet[str]]: +) -> tuple[set[str], set[str]]: """Get the database aliases that need to be setup, and the subset that needs to be serialized.""" # Code derived from django.test.utils.DiscoverRunner.get_databases(). @@ -736,7 +722,7 @@ def __call__( *, using: str = ..., execute: bool = ..., - ) -> ContextManager[list[Callable[[], Any]]]: + ) -> AbstractContextManager[list[Callable[[], Any]]]: pass # pragma: no cover diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index e8e629f4..00c3744b 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -12,8 +12,10 @@ import pathlib import sys import types +from collections.abc import Generator +from contextlib import AbstractContextManager from functools import reduce -from typing import TYPE_CHECKING, ContextManager, Generator, List, NoReturn +from typing import TYPE_CHECKING, NoReturn import pytest @@ -259,7 +261,7 @@ def _get_boolean_value( ) from None -report_header_key = pytest.StashKey[List[str]]() +report_header_key = pytest.StashKey[list[str]]() @pytest.hookimpl() @@ -837,13 +839,13 @@ def _blocking_wrapper(*args, **kwargs) -> NoReturn: '"db" or "transactional_db" fixtures to enable it.' ) - def unblock(self) -> ContextManager[None]: + def unblock(self) -> AbstractContextManager[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) -> ContextManager[None]: + def block(self) -> AbstractContextManager[None]: """Disable access to the Django database.""" self._save_active_wrapper() self._dj_db_wrapper.ensure_connection = self._blocking_wrapper diff --git a/pytest_django/runner.py b/pytest_django/runner.py index d9032622..1b6571cc 100644 --- a/pytest_django/runner.py +++ b/pytest_django/runner.py @@ -1,5 +1,6 @@ from argparse import ArgumentParser -from typing import Any, Iterable +from collections.abc import Iterable +from typing import Any class TestRunner: diff --git a/pytest_django_test/db_helpers.py b/pytest_django_test/db_helpers.py index 712af0d3..b9efe86d 100644 --- a/pytest_django_test/db_helpers.py +++ b/pytest_django_test/db_helpers.py @@ -3,7 +3,7 @@ import os import sqlite3 import subprocess -from typing import Mapping +from collections.abc import Mapping import pytest from django.conf import settings diff --git a/tests/test_database.py b/tests/test_database.py index c6389756..2fec1352 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Generator +from collections.abc import Generator import pytest from django.db import connection, transaction diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index f88ed802..709ae6c9 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -5,8 +5,8 @@ """ import socket +from collections.abc import Generator from contextlib import contextmanager -from typing import Generator from urllib.error import HTTPError from urllib.request import urlopen diff --git a/tests/test_runner.py b/tests/test_runner.py index 71fd7160..a0bee059 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -6,7 +6,7 @@ @pytest.mark.parametrize( - "kwargs, expected", + ("kwargs", "expected"), [ ({}, call(["tests"])), ({"verbosity": 0}, call(["--quiet", "tests"])), diff --git a/tox.ini b/tox.ini index aed0b8a3..e064bc0c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,10 @@ [tox] envlist = py313-dj{main,52,51}-postgres - py312-dj{main,52,51,50,42}-postgres - py311-dj{main,52,51,50,42}-postgres - py310-dj{main,52,51,50,42}-postgres + py312-dj{main,52,51,42}-postgres + py311-dj{main,52,51,42}-postgres + py310-dj{main,52,51,42}-postgres py39-dj42-postgres - py38-dj42-postgres linting [testenv] From 484ce30af3da467e0a19ca5aeeffba79da5aaf1d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 2 Jun 2025 22:36:29 +0300 Subject: [PATCH 04/34] pyproject.toml: switch from extras to dependency groups See https://packaging.python.org/en/latest/specifications/dependency-groups/ More suitable for internal stuff than extras which are exposed externally. Also moves all development dependency declarations from tox.ini to be in a single place, pyproject.toml. --- .github/workflows/main.yml | 2 +- pyproject.toml | 19 ++++++++++++++++++- tox.ini | 26 +++++++++----------------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6556866f..ceca18b9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -50,7 +50,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install tox==4.11.1 + pip install tox==4.26.0 - name: Run tox run: tox -e ${{ matrix.name }} diff --git a/pyproject.toml b/pyproject.toml index 61f2c006..2855ff4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ classifiers = [ dependencies = [ "pytest>=7.0.0", ] -[project.optional-dependencies] +[dependency-groups] docs = [ "sphinx", "sphinx_rtd_theme", @@ -49,6 +49,23 @@ testing = [ "Django", "django-configurations>=2.0", ] +coverage = [ + "coverage[toml]", + "coverage-enable-subprocess", +] +postgres = [ + "psycopg[binary]", +] +mysql = [ + "mysqlclient==2.1.0", +] +xdist = [ + "pytest-xdist", +] +linting = [ + "ruff==0.9.5", + "mypy==1.15.0", +] [project.urls] Documentation = "https://pytest-django.readthedocs.io/" Repository = "https://github.com/pytest-dev/pytest-django" diff --git a/tox.ini b/tox.ini index e064bc0c..a892c38e 100644 --- a/tox.ini +++ b/tox.ini @@ -8,22 +8,19 @@ envlist = linting [testenv] -extras = testing +dependency_groups = + testing + coverage: coverage + mysql: mysql + postgres: postgres + xdist: xdist deps = djmain: https://github.com/django/django/archive/main.tar.gz dj52: Django>=5.2a1,<6.0 dj51: Django>=5.1,<5.2 dj50: Django>=5.0,<5.1 dj42: Django>=4.2,<4.3 - - mysql: mysqlclient==2.1.0 - - postgres: psycopg[binary] - coverage: coverage[toml] - coverage: coverage-enable-subprocess - pytestmin: pytest>=7.0,<7.1 - xdist: pytest-xdist>=1.15 setenv = mysql: DJANGO_SETTINGS_MODULE=pytest_django_test.settings_mysql @@ -46,26 +43,21 @@ commands = coverage: coverage xml [testenv:linting] -extras = -deps = - ruff==0.9.5 - mypy==1.15.0 +dependency_groups = linting commands = ruff check --diff {posargs:pytest_django pytest_django_test tests} ruff format --quiet --diff {posargs:pytest_django pytest_django_test tests} mypy {posargs:pytest_django pytest_django_test tests} [testenv:doc8] -extras = basepython = python3 skip_install = true +dependency_groups = docs deps = - sphinx doc8 commands = doc8 docs/ [testenv:docs] -deps = -extras = docs +dependency_groups = docs commands = sphinx-build -n -W -b html -d docs/_build/doctrees docs docs/_build/html From e181bc1119def178f0e1b6dc482b0fe11438a4ad Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 2 Jun 2025 23:04:47 +0300 Subject: [PATCH 05/34] Makefile: remove Doesn't much over running the individual commands. And the `test` one sometimes needs updating which is annoying to remember. --- Makefile | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 Makefile diff --git a/Makefile b/Makefile deleted file mode 100644 index af72c983..00000000 --- a/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -.PHONY: docs test clean fix - -test: - tox -e py-dj42-sqlite_file - -docs: - tox -e docs - -fix: - ruff check --fix pytest_django pytest_django_test tests - -clean: - rm -rf bin include/ lib/ man/ pytest_django.egg-info/ build/ From cdef84f1f991d0cdd58bb758b2d1f241399198e9 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Mon, 2 Jun 2025 16:36:27 -0400 Subject: [PATCH 06/34] Fix ruff PT001 warning (#1204) --- pyproject.toml | 1 - pytest_django/fixtures.py | 34 +++++++++++++++++----------------- pytest_django/plugin.py | 6 +++--- tests/conftest.py | 2 +- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2855ff4f..6f7842ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -154,7 +154,6 @@ extend-select = [ ignore = [ "PLR0913", # Too many arguments in function definition "PLR2004", # Magic value used in comparison, consider replacing 3 with a constant variable - "PT001", # Use `@pytest.fixture()` over `@pytest.fixture` "PT023", # Use `@pytest.mark.django_db()` over `@pytest.mark.django_db` ] diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index c1bf9ca1..115dc4cc 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -201,7 +201,7 @@ def django_db_setup( ) -@pytest.fixture() +@pytest.fixture def _django_db_helper( request: pytest.FixtureRequest, django_db_setup: None, @@ -365,7 +365,7 @@ def _set_suffix_to_test_databases(suffix: str) -> None: # ############### User visible fixtures ################ -@pytest.fixture() +@pytest.fixture def db(_django_db_helper: None) -> None: """Require a django test database. @@ -382,7 +382,7 @@ def db(_django_db_helper: None) -> None: # The `_django_db_helper` fixture checks if `db` is requested. -@pytest.fixture() +@pytest.fixture def transactional_db(_django_db_helper: None) -> None: """Require a django test database with transaction support. @@ -398,7 +398,7 @@ def transactional_db(_django_db_helper: None) -> None: # The `_django_db_helper` fixture checks if `transactional_db` is requested. -@pytest.fixture() +@pytest.fixture def django_db_reset_sequences( _django_db_helper: None, transactional_db: None, @@ -414,7 +414,7 @@ def django_db_reset_sequences( # is requested. -@pytest.fixture() +@pytest.fixture def django_db_serialized_rollback( _django_db_helper: None, db: None, @@ -435,7 +435,7 @@ def django_db_serialized_rollback( # is requested. -@pytest.fixture() +@pytest.fixture def client() -> django.test.Client: """A Django test client instance.""" skip_if_no_django() @@ -445,7 +445,7 @@ def client() -> django.test.Client: return Client() -@pytest.fixture() +@pytest.fixture def async_client() -> django.test.AsyncClient: """A Django test async client instance.""" skip_if_no_django() @@ -455,7 +455,7 @@ def async_client() -> django.test.AsyncClient: return AsyncClient() -@pytest.fixture() +@pytest.fixture def django_user_model(db: None): """The class of Django's user model.""" from django.contrib.auth import get_user_model @@ -463,14 +463,14 @@ def django_user_model(db: None): return get_user_model() -@pytest.fixture() +@pytest.fixture def django_username_field(django_user_model) -> str: """The fieldname for the username used with Django's user model.""" field: str = django_user_model.USERNAME_FIELD return field -@pytest.fixture() +@pytest.fixture def admin_user( db: None, django_user_model, @@ -501,7 +501,7 @@ def admin_user( return user -@pytest.fixture() +@pytest.fixture def admin_client( db: None, admin_user, @@ -514,7 +514,7 @@ def admin_client( return client -@pytest.fixture() +@pytest.fixture def rf() -> django.test.RequestFactory: """RequestFactory instance""" skip_if_no_django() @@ -524,7 +524,7 @@ def rf() -> django.test.RequestFactory: return RequestFactory() -@pytest.fixture() +@pytest.fixture def async_rf() -> django.test.AsyncRequestFactory: """AsyncRequestFactory instance""" skip_if_no_django() @@ -569,7 +569,7 @@ def finalize(self) -> None: del self._to_restore[:] -@pytest.fixture() +@pytest.fixture def settings(): """A Django settings object which restores changes after the testrun""" skip_if_no_django() @@ -702,13 +702,13 @@ def _assert_num_queries( pytest.fail(msg) -@pytest.fixture() +@pytest.fixture def django_assert_num_queries(pytestconfig: pytest.Config) -> DjangoAssertNumQueries: """Allows to check for an expected number of DB queries.""" return partial(_assert_num_queries, pytestconfig) -@pytest.fixture() +@pytest.fixture def django_assert_max_num_queries(pytestconfig: pytest.Config) -> DjangoAssertNumQueries: """Allows to check for an expected maximum number of DB queries.""" return partial(_assert_num_queries, pytestconfig, exact=False) @@ -726,7 +726,7 @@ def __call__( pass # pragma: no cover -@pytest.fixture() +@pytest.fixture def django_capture_on_commit_callbacks() -> DjangoCaptureOnCommitCallbacks: """Captures transaction.on_commit() callbacks for the given database connection.""" from django.test import TestCase diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 00c3744b..9bab8971 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -605,7 +605,7 @@ def _dj_autoclear_mailbox() -> None: mail.outbox.clear() -@pytest.fixture() +@pytest.fixture def mailoutbox( django_mail_patch_dns: None, _dj_autoclear_mailbox: None, @@ -620,7 +620,7 @@ def mailoutbox( return [] -@pytest.fixture() +@pytest.fixture def django_mail_patch_dns( monkeypatch: pytest.MonkeyPatch, django_mail_dnsname: str, @@ -631,7 +631,7 @@ def django_mail_patch_dns( monkeypatch.setattr(mail.message, "DNS_NAME", django_mail_dnsname) -@pytest.fixture() +@pytest.fixture def django_mail_dnsname() -> str: """Return server dns name for using in email messages.""" return "fake-tests.example.com" diff --git a/tests/conftest.py b/tests/conftest.py index e3bfa1f4..7bef403f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -46,7 +46,7 @@ def pytester(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch) -> pyte return pytester -@pytest.fixture() +@pytest.fixture def django_pytester( request: pytest.FixtureRequest, pytester: pytest.Pytester, From 587381c586198431d939b47294dfdb1d4e37fa14 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Mon, 2 Jun 2025 17:18:10 -0400 Subject: [PATCH 07/34] Fix CI (#1210) --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ceca18b9..e0289b28 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,9 +4,9 @@ on: push: branches: - main + tags: + - "*" pull_request: - branches: - - main concurrency: group: ci-main-${{ github.ref }} From 5a213d27f9df595f3bdbc2281f23040371b7c6ae Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Mon, 2 Jun 2025 17:24:29 -0400 Subject: [PATCH 08/34] Fix ruff PT023 warning (#1205) --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6f7842ba..46145ee4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -154,7 +154,6 @@ extend-select = [ ignore = [ "PLR0913", # Too many arguments in function definition "PLR2004", # Magic value used in comparison, consider replacing 3 with a constant variable - "PT023", # Use `@pytest.mark.django_db()` over `@pytest.mark.django_db` ] [tool.ruff.lint.isort] From 007b7fa70057a303bb4c290a1b6834ef2bb4d1af Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Tue, 3 Jun 2025 05:37:06 -0400 Subject: [PATCH 09/34] Adds github actions static analysis (#1211) --- .github/zizmor.yml | 6 ++++++ pyproject.toml | 3 ++- tox.ini | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .github/zizmor.yml diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 00000000..2ed61128 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,6 @@ +rules: + unpinned-uses: + config: + policies: + actions/*: ref-pin + codecov/codecov-action: ref-pin diff --git a/pyproject.toml b/pyproject.toml index 46145ee4..36cdfe32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,8 +63,9 @@ xdist = [ "pytest-xdist", ] linting = [ - "ruff==0.9.5", "mypy==1.15.0", + "ruff==0.9.5", + "zizmor==1.9.0", ] [project.urls] Documentation = "https://pytest-django.readthedocs.io/" diff --git a/tox.ini b/tox.ini index a892c38e..fe0e228b 100644 --- a/tox.ini +++ b/tox.ini @@ -48,6 +48,7 @@ commands = ruff check --diff {posargs:pytest_django pytest_django_test tests} ruff format --quiet --diff {posargs:pytest_django pytest_django_test tests} mypy {posargs:pytest_django pytest_django_test tests} + zizmor --persona=regular .github/workflows/deploy.yml .github/workflows/main.yml [testenv:doc8] basepython = python3 From 4f15ae771559f12c52619311723eb13e540ca76e Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Tue, 3 Jun 2025 05:57:38 -0400 Subject: [PATCH 10/34] Pedantic static analisis for github actions (#1212) --- .github/workflows/main.yml | 4 +++- tox.ini | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e0289b28..3fab805b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -53,7 +53,9 @@ jobs: pip install tox==4.26.0 - name: Run tox - run: tox -e ${{ matrix.name }} + run: tox -e "${MATRIX_NAME}" + env: + MATRIX_NAME: ${{ matrix.name }} - name: Report coverage if: contains(matrix.name, 'coverage') diff --git a/tox.ini b/tox.ini index fe0e228b..ccd5e381 100644 --- a/tox.ini +++ b/tox.ini @@ -48,7 +48,7 @@ commands = ruff check --diff {posargs:pytest_django pytest_django_test tests} ruff format --quiet --diff {posargs:pytest_django pytest_django_test tests} mypy {posargs:pytest_django pytest_django_test tests} - zizmor --persona=regular .github/workflows/deploy.yml .github/workflows/main.yml + zizmor --persona=pedantic .github/workflows/deploy.yml .github/workflows/main.yml [testenv:doc8] basepython = python3 From 6c0e6b240b364e44ae40eab81eb23e881b3723a7 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Tue, 3 Jun 2025 15:52:33 -0400 Subject: [PATCH 11/34] Fix CI concurrency (#1213) --- .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 3fab805b..74c5f63e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,7 +9,7 @@ on: pull_request: concurrency: - group: ci-main-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} env: From f7f1ee92ac075d2479cc7ca8126251804f499481 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Thu, 5 Jun 2025 12:18:09 -0400 Subject: [PATCH 12/34] Simplify tox environment (#1215) --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 74c5f63e..921f81ca 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,6 +25,8 @@ jobs: timeout-minutes: 15 permissions: contents: read + env: + TOXENV: ${{ matrix.name }} steps: - uses: actions/checkout@v4 with: @@ -53,9 +55,7 @@ jobs: pip install tox==4.26.0 - name: Run tox - run: tox -e "${MATRIX_NAME}" - env: - MATRIX_NAME: ${{ matrix.name }} + run: tox - name: Report coverage if: contains(matrix.name, 'coverage') From ab218f3c03c3cec4d516db4494ffa56199805872 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Thu, 5 Jun 2025 16:02:39 -0400 Subject: [PATCH 13/34] Adds zizmor sarif (#1214) Co-authored-by: Ran Benita --- .github/workflows/main.yml | 8 ++++++++ .github/zizmor.yml | 1 + .gitignore | 1 + tox.ini | 2 +- 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 921f81ca..a577b387 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,6 +25,7 @@ jobs: timeout-minutes: 15 permissions: contents: read + security-events: write env: TOXENV: ${{ matrix.name }} steps: @@ -57,6 +58,13 @@ jobs: - name: Run tox run: tox + - name: Upload zizmor SARIF report into the GitHub repo code scanning + if: contains(matrix.name, 'linting') + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: zizmor.sarif + category: zizmor + - name: Report coverage if: contains(matrix.name, 'coverage') uses: codecov/codecov-action@v5 diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 2ed61128..a935769a 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -4,3 +4,4 @@ rules: policies: actions/*: ref-pin codecov/codecov-action: ref-pin + github/*: ref-pin diff --git a/.gitignore b/.gitignore index 35f1856e..27011bfa 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ _build *.egg # autogenerated by setuptools-scm /pytest_django/_version.py +zizmor.sarif diff --git a/tox.ini b/tox.ini index ccd5e381..59d4cb57 100644 --- a/tox.ini +++ b/tox.ini @@ -48,7 +48,7 @@ commands = ruff check --diff {posargs:pytest_django pytest_django_test tests} ruff format --quiet --diff {posargs:pytest_django pytest_django_test tests} mypy {posargs:pytest_django pytest_django_test tests} - zizmor --persona=pedantic .github/workflows/deploy.yml .github/workflows/main.yml + python -c "import subprocess, sys; sys.exit(subprocess.call('zizmor --persona=pedantic --format sarif .github/workflows/deploy.yml .github/workflows/main.yml > zizmor.sarif', shell=True))" [testenv:doc8] basepython = python3 From d07a96708db3da577d24ca2046f1581239003619 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 9 Jun 2025 18:10:35 +0200 Subject: [PATCH 14/34] tests: harden Test_django_db_blocker tests (#803) --- tests/test_fixtures.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 709ae6c9..94f79fa6 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -725,7 +725,7 @@ class Test_django_db_blocker: def test_block_manually(self, django_db_blocker: DjangoDbBlocker) -> None: try: django_db_blocker.block() - with pytest.raises(RuntimeError): + with pytest.raises(RuntimeError, match="^Database access not allowed,"): Item.objects.exists() finally: django_db_blocker.restore() @@ -733,7 +733,7 @@ def test_block_manually(self, django_db_blocker: DjangoDbBlocker) -> None: @pytest.mark.django_db def test_block_with_block(self, django_db_blocker: DjangoDbBlocker) -> None: with django_db_blocker.block(): - with pytest.raises(RuntimeError): + with pytest.raises(RuntimeError, match="^Database access not allowed,"): Item.objects.exists() def test_unblock_manually(self, django_db_blocker: DjangoDbBlocker) -> None: From ba335ce2c4ac825738f45d1899b1d54aa9b2fdd7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 10:49:06 +0200 Subject: [PATCH 15/34] build(deps): bump hynek/build-and-inspect-python-package (#1218) Bumps [hynek/build-and-inspect-python-package](https://github.com/hynek/build-and-inspect-python-package) from 2.12.0 to 2.13.0. - [Release notes](https://github.com/hynek/build-and-inspect-python-package/releases) - [Changelog](https://github.com/hynek/build-and-inspect-python-package/blob/main/CHANGELOG.md) - [Commits](https://github.com/hynek/build-and-inspect-python-package/compare/b5076c307dc91924a82ad150cdd1533b444d3310...c52c3a4710070b50470d903818a7b25115dcd076) --- updated-dependencies: - dependency-name: hynek/build-and-inspect-python-package dependency-version: 2.13.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9690a3f7..f06318be 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -19,7 +19,7 @@ jobs: persist-credentials: false - name: Build and Check Package - uses: hynek/build-and-inspect-python-package@b5076c307dc91924a82ad150cdd1533b444d3310 # v2.12.0 + uses: hynek/build-and-inspect-python-package@c52c3a4710070b50470d903818a7b25115dcd076 # v2.13.0 deploy: if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pytest-django' From 79ca7d9de02461e85c52d1bc8060e04d65bdb991 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Aug 2025 10:29:58 -0400 Subject: [PATCH 16/34] build(deps): bump re-actors/alls-green (#1219) Bumps [re-actors/alls-green](https://github.com/re-actors/alls-green) from 223e4bb7a751b91f43eda76992bcfbf23b8b0302 to 2765efec08f0fd63e83ad900f5fd75646be69ff6. - [Release notes](https://github.com/re-actors/alls-green/releases) - [Commits](https://github.com/re-actors/alls-green/compare/223e4bb7a751b91f43eda76992bcfbf23b8b0302...2765efec08f0fd63e83ad900f5fd75646be69ff6) --- updated-dependencies: - dependency-name: re-actors/alls-green dependency-version: 2765efec08f0fd63e83ad900f5fd75646be69ff6 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .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 a577b387..fd4b98c3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -161,6 +161,6 @@ jobs: steps: - name: Decide whether the needed jobs succeeded or failed - uses: re-actors/alls-green@223e4bb7a751b91f43eda76992bcfbf23b8b0302 + uses: re-actors/alls-green@2765efec08f0fd63e83ad900f5fd75646be69ff6 with: jobs: ${{ toJSON(needs) }} From bd9408f85a02a4e66c2d36755195d55cac952233 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Sat, 2 Aug 2025 15:41:59 -0400 Subject: [PATCH 17/34] Adds uv to tox (#1216) --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fd4b98c3..ec746a21 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -52,8 +52,8 @@ jobs: - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install tox==4.26.0 + python -m pip install uv + uv tool install tox==4.26.0 --with tox-uv - name: Run tox run: tox From 55b8cc8e60879338fbaff8b1e23f001f97277816 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Sat, 2 Aug 2025 15:53:14 -0400 Subject: [PATCH 18/34] Skip test that cannot be ran in parallel (#1217) --- tests/test_fixtures.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 94f79fa6..600ac626 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -4,6 +4,7 @@ fixtures are tested in test_database. """ +import os import socket from collections.abc import Generator from contextlib import contextmanager @@ -443,6 +444,7 @@ def test_set_non_existent(settings): class TestLiveServer: + @pytest.mark.skipif("PYTEST_XDIST_WORKER" in os.environ, reason="xdist in use") def test_settings_before(self) -> None: from django.conf import settings @@ -458,6 +460,7 @@ def test_url(https://github.com/self,%20live_server) -> None: def test_change_settings(self, live_server, settings) -> None: assert live_server.url == force_str(live_server) + @pytest.mark.skipif("PYTEST_XDIST_WORKER" in os.environ, reason="xdist in use") def test_settings_restored(self) -> None: """Ensure that settings are restored after test_settings_before.""" from django.conf import settings From dce4ef5fb01d64c97ee0c42fa84bb6406a982f8a Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Sat, 2 Aug 2025 15:53:54 -0400 Subject: [PATCH 19/34] Update tox version (#1220) --- .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 ec746a21..b6fb5cb8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -53,7 +53,7 @@ jobs: - name: Install dependencies run: | python -m pip install uv - uv tool install tox==4.26.0 --with tox-uv + uv tool install tox==4.28.4 --with tox-uv - name: Run tox run: tox From 8dd6acfdb2ce1d2a610aeb387adb237e850affa2 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Tue, 12 Aug 2025 16:11:06 +0100 Subject: [PATCH 20/34] Create .editorconfig (#1225) --- .editorconfig | 37 +++++++++ .editorconfig-checker.json | 6 ++ .readthedocs.yml | 10 +-- docs/faq.rst | 2 +- docs/managing_python_path.rst | 2 +- docs/usage.rst | 2 +- pyproject.toml | 5 +- tests/test_django_configurations.py | 18 ++--- tests/test_django_settings_module.py | 113 +++++++++++++++++---------- tests/test_fixtures.py | 20 ++--- tox.ini | 1 + 11 files changed, 146 insertions(+), 70 deletions(-) create mode 100644 .editorconfig create mode 100644 .editorconfig-checker.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..1e469181 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,37 @@ +# https://editorconfig.org/ + +root = true + +[*] +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true +end_of_line = lf +charset = utf-8 + +[*.py] +max_line_length = 99 + +[*.yml] +indent_size = 2 + +[*.ini] +indent_size = 2 + +[*.json] +indent_size = 2 +insert_final_newline = unset + +[*.rst] +indent_size = unset +insert_final_newline = unset + +[*.bat] +indent_style = tab + +[LICENSE] +indent_size = unset + +[docs/Makefile] +indent_style = tab diff --git a/.editorconfig-checker.json b/.editorconfig-checker.json new file mode 100644 index 00000000..3fa0e744 --- /dev/null +++ b/.editorconfig-checker.json @@ -0,0 +1,6 @@ +{ + "Exclude": ["pytest_django/fixtures.py"], + "Disable": { + "MaxLineLength": true + } +} diff --git a/.readthedocs.yml b/.readthedocs.yml index ba6a262b..c13e1e00 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -9,11 +9,11 @@ build: python: "3" python: - install: - - method: pip - path: . - extra_requirements: - - docs + install: + - method: pip + path: . + extra_requirements: + - docs formats: - epub diff --git a/docs/faq.rst b/docs/faq.rst index 8ad588b0..5a4f4d88 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -79,7 +79,7 @@ How can I use ``manage.py test`` with pytest-django? ---------------------------------------------------- pytest-django is designed to work with the ``pytest`` command, but if you -really need integration with ``manage.py test``, you can add this class path +really need integration with ``manage.py test``, you can add this class path in your Django settings: .. code-block:: python diff --git a/docs/managing_python_path.rst b/docs/managing_python_path.rst index 37488662..561ef822 100644 --- a/docs/managing_python_path.rst +++ b/docs/managing_python_path.rst @@ -87,7 +87,7 @@ You can explicitly add paths to the Python search path using pytest's Example: project with src layout ```````````````````````````````` -For a Django package using the ``src`` layout, with test settings located in a +For a Django package using the ``src`` layout, with test settings located in a ``tests`` package at the top level:: myproj diff --git a/docs/usage.rst b/docs/usage.rst index edfead5e..6e9822c6 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -33,7 +33,7 @@ You can switch it on in `pytest.ini`:: [pytest] FAIL_INVALID_TEMPLATE_VARS = True - + Additional pytest.ini settings ------------------------------ diff --git a/pyproject.toml b/pyproject.toml index 36cdfe32..883143af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [build-system] requires = [ - "setuptools>=61.0.0", - "setuptools-scm[toml]>=5.0.0", + "setuptools>=61.0.0", + "setuptools-scm[toml]>=5.0.0", ] build-backend = "setuptools.build_meta" @@ -63,6 +63,7 @@ xdist = [ "pytest-xdist", ] linting = [ + "editorconfig-checker==3.2.1", "mypy==1.15.0", "ruff==0.9.5", "zizmor==1.9.0", diff --git a/tests/test_django_configurations.py b/tests/test_django_configurations.py index 88d89cf6..6e1a2b6d 100644 --- a/tests/test_django_configurations.py +++ b/tests/test_django_configurations.py @@ -58,9 +58,9 @@ def test_dc_env_overrides_ini(pytester: pytest.Pytester, monkeypatch: pytest.Mon pytester.makeini( """ - [pytest] - DJANGO_SETTINGS_MODULE = DO_NOT_USE_ini - DJANGO_CONFIGURATION = DO_NOT_USE_ini + [pytest] + DJANGO_SETTINGS_MODULE = DO_NOT_USE_ini + DJANGO_CONFIGURATION = DO_NOT_USE_ini """ ) pkg = pytester.mkpydir("tpkg") @@ -91,9 +91,9 @@ def test_dc_ini(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch) -> N pytester.makeini( """ - [pytest] - DJANGO_SETTINGS_MODULE = tpkg.settings_ini - DJANGO_CONFIGURATION = MySettings + [pytest] + DJANGO_SETTINGS_MODULE = tpkg.settings_ini + DJANGO_CONFIGURATION = MySettings """ ) pkg = pytester.mkpydir("tpkg") @@ -125,9 +125,9 @@ def test_dc_option(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch) - pytester.makeini( """ - [pytest] - DJANGO_SETTINGS_MODULE = DO_NOT_USE_ini - DJANGO_CONFIGURATION = DO_NOT_USE_ini + [pytest] + DJANGO_SETTINGS_MODULE = DO_NOT_USE_ini + DJANGO_CONFIGURATION = DO_NOT_USE_ini """ ) pkg = pytester.mkpydir("tpkg") diff --git a/tests/test_django_settings_module.py b/tests/test_django_settings_module.py index fa4db778..68d587e9 100644 --- a/tests/test_django_settings_module.py +++ b/tests/test_django_settings_module.py @@ -22,8 +22,8 @@ def test_ds_ini(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch) -> N monkeypatch.delenv("DJANGO_SETTINGS_MODULE") pytester.makeini( """ - [pytest] - DJANGO_SETTINGS_MODULE = tpkg.settings_ini + [pytest] + DJANGO_SETTINGS_MODULE = tpkg.settings_ini """ ) pkg = pytester.mkpydir("tpkg") @@ -72,8 +72,8 @@ def test_ds_option(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch) - monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "DO_NOT_USE_env") pytester.makeini( """ - [pytest] - DJANGO_SETTINGS_MODULE = DO_NOT_USE_ini + [pytest] + DJANGO_SETTINGS_MODULE = DO_NOT_USE_ini """ ) pkg = pytester.mkpydir("tpkg") @@ -101,8 +101,8 @@ def test_ds_env_override_ini(pytester: pytest.Pytester, monkeypatch: pytest.Monk monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "tpkg.settings_env") pytester.makeini( """\ - [pytest] - DJANGO_SETTINGS_MODULE = DO_NOT_USE_ini + [pytest] + DJANGO_SETTINGS_MODULE = DO_NOT_USE_ini """ ) pkg = pytester.mkpydir("tpkg") @@ -166,8 +166,10 @@ def test_ds_in_pytest_configure( def pytest_configure(): if not settings.configured: - os.environ.setdefault('DJANGO_SETTINGS_MODULE', - 'tpkg.settings_ds') + os.environ.setdefault( + 'DJANGO_SETTINGS_MODULE', + 'tpkg.settings_ds', + ) """ ) @@ -196,18 +198,24 @@ def test_django_settings_configure( p = pytester.makepyfile( run=""" - from django.conf import settings - settings.configure(SECRET_KEY='set from settings.configure()', - DATABASES={'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ':memory:' - }}, - INSTALLED_APPS=['django.contrib.auth', - 'django.contrib.contenttypes',]) - - import pytest - - pytest.main() + from django.conf import settings + settings.configure( + SECRET_KEY='set from settings.configure()', + DATABASES={ + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:' + } + }, + INSTALLED_APPS=[ + 'django.contrib.auth', + 'django.contrib.contenttypes', + ] + ) + + import pytest + + pytest.main() """ ) @@ -249,12 +257,19 @@ def test_settings_in_hook(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyP from django.conf import settings def pytest_configure(): - settings.configure(SECRET_KEY='set from pytest_configure', - DATABASES={'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ':memory:'}}, - INSTALLED_APPS=['django.contrib.auth', - 'django.contrib.contenttypes',]) + settings.configure( + SECRET_KEY='set from pytest_configure', + DATABASES={ + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:' + } + }, + INSTALLED_APPS=[ + 'django.contrib.auth', + 'django.contrib.contenttypes', + ] + ) """ ) pytester.makepyfile( @@ -305,13 +320,20 @@ def test_debug_false_by_default( from django.conf import settings def pytest_configure(): - settings.configure(SECRET_KEY='set from pytest_configure', - DEBUG=True, - DATABASES={'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ':memory:'}}, - INSTALLED_APPS=['django.contrib.auth', - 'django.contrib.contenttypes',]) + settings.configure( + SECRET_KEY='set from pytest_configure', + DEBUG=True, + DATABASES={ + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:' + } + }, + INSTALLED_APPS=[ + 'django.contrib.auth', + 'django.contrib.contenttypes', + ] + ) """ ) @@ -336,8 +358,8 @@ def test_django_debug_mode_true_false( monkeypatch.delenv("DJANGO_SETTINGS_MODULE") pytester.makeini( f""" - [pytest] - django_debug_mode = {django_debug_mode} + [pytest] + django_debug_mode = {django_debug_mode} """ ) pytester.makeconftest( @@ -345,13 +367,20 @@ def test_django_debug_mode_true_false( 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.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) ) diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 600ac626..80578959 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -402,10 +402,12 @@ def receiver(sender, **kwargs): '<>')} fmt_dict.update(kwargs) - print('Setting changed: ' - 'enter=%(enter)s,setting=%(setting)s,' - 'value=%(value)s,actual_value=%(actual_value)s' - % fmt_dict) + print( + 'Setting changed: ' + 'enter=%(enter)s,setting=%(setting)s,' + 'value=%(value)s,actual_value=%(actual_value)s' + % fmt_dict + ) setting_changed.connect(receiver, weak=False) @@ -417,7 +419,7 @@ def test_set(settings): def test_set_non_existent(settings): settings.FOOBAR = 'abc123' - """ + """ ) result = django_pytester.runpytest_subprocess("--tb=short", "-v", "-s") @@ -785,8 +787,7 @@ def django_mail_dnsname(): return 'from.django_mail_dnsname' def test_mailbox_inner(mailoutbox): - mail.send_mail('subject', 'body', 'from@example.com', - ['to@example.com']) + mail.send_mail('subject', 'body', 'from@example.com', ['to@example.com']) m = mailoutbox[0] message = m.message() assert message['Message-ID'].endswith('@from.django_mail_dnsname>') @@ -817,8 +818,9 @@ def mocked_make_msgid(*args, **kwargs): mocked_make_msgid.called = [] monkeypatch.setattr(mail.message, 'make_msgid', mocked_make_msgid) - mail.send_mail('subject', 'body', 'from@example.com', - ['to@example.com']) + mail.send_mail( + 'subject', 'body', 'from@example.com', ['to@example.com'] + ) m = mailoutbox[0] assert len(mocked_make_msgid.called) == 1 diff --git a/tox.ini b/tox.ini index 59d4cb57..a9c14a89 100644 --- a/tox.ini +++ b/tox.ini @@ -48,6 +48,7 @@ commands = ruff check --diff {posargs:pytest_django pytest_django_test tests} ruff format --quiet --diff {posargs:pytest_django pytest_django_test tests} mypy {posargs:pytest_django pytest_django_test tests} + ec {posargs:pytest_django pytest_django_test tests} python -c "import subprocess, sys; sys.exit(subprocess.call('zizmor --persona=pedantic --format sarif .github/workflows/deploy.yml .github/workflows/main.yml > zizmor.sarif', shell=True))" [testenv:doc8] From 9c97a67ada953cf49527aed9ebf6f71ba10eb960 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Tue, 12 Aug 2025 19:55:25 +0100 Subject: [PATCH 21/34] Fix editorconfig check for files that don't contain python code (#1226) --- .editorconfig-checker.json | 7 ++++++- tox.ini | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.editorconfig-checker.json b/.editorconfig-checker.json index 3fa0e744..0410ab8e 100644 --- a/.editorconfig-checker.json +++ b/.editorconfig-checker.json @@ -1,5 +1,10 @@ { - "Exclude": ["pytest_django/fixtures.py"], + "Exclude": [ + "pytest_django/fixtures.py", + ".tox/*", + ".ruff_cache/*", + "pytest_django.egg-info/*" + ], "Disable": { "MaxLineLength": true } diff --git a/tox.ini b/tox.ini index a9c14a89..1caaf78d 100644 --- a/tox.ini +++ b/tox.ini @@ -48,7 +48,7 @@ commands = ruff check --diff {posargs:pytest_django pytest_django_test tests} ruff format --quiet --diff {posargs:pytest_django pytest_django_test tests} mypy {posargs:pytest_django pytest_django_test tests} - ec {posargs:pytest_django pytest_django_test tests} + ec . python -c "import subprocess, sys; sys.exit(subprocess.call('zizmor --persona=pedantic --format sarif .github/workflows/deploy.yml .github/workflows/main.yml > zizmor.sarif', shell=True))" [testenv:doc8] From d1607189796bbf5822fdfbf8f65cf5b52460e53e Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Wed, 13 Aug 2025 13:32:02 +0100 Subject: [PATCH 22/34] Fix local ec linter issue (#1228) --- .editorconfig-checker.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.editorconfig-checker.json b/.editorconfig-checker.json index 0410ab8e..1e9487fe 100644 --- a/.editorconfig-checker.json +++ b/.editorconfig-checker.json @@ -3,7 +3,9 @@ "pytest_django/fixtures.py", ".tox/*", ".ruff_cache/*", - "pytest_django.egg-info/*" + "pytest_django.egg-info/*", + "__pycache__/*", + "zizmor.sarif" ], "Disable": { "MaxLineLength": true From 3f36d499b9c11036fa10d257cbdb719f3041938c Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Wed, 13 Aug 2025 13:37:05 +0100 Subject: [PATCH 23/34] Modernize pyproject + tox (#1227) --- pyproject.toml | 4 +++- tox.ini | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 883143af..56b3cf2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,8 @@ classifiers = [ dependencies = [ "pytest>=7.0.0", ] -[dependency-groups] + +[project.optional-dependencies] docs = [ "sphinx", "sphinx_rtd_theme", @@ -68,6 +69,7 @@ linting = [ "ruff==0.9.5", "zizmor==1.9.0", ] + [project.urls] Documentation = "https://pytest-django.readthedocs.io/" Repository = "https://github.com/pytest-dev/pytest-django" diff --git a/tox.ini b/tox.ini index 1caaf78d..e8d739e6 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ envlist = linting [testenv] -dependency_groups = +extras = testing coverage: coverage mysql: mysql @@ -43,7 +43,7 @@ commands = coverage: coverage xml [testenv:linting] -dependency_groups = linting +extras = linting commands = ruff check --diff {posargs:pytest_django pytest_django_test tests} ruff format --quiet --diff {posargs:pytest_django pytest_django_test tests} @@ -54,12 +54,12 @@ commands = [testenv:doc8] basepython = python3 skip_install = true -dependency_groups = docs +extras = docs deps = doc8 commands = doc8 docs/ [testenv:docs] -dependency_groups = docs +extras = docs commands = sphinx-build -n -W -b html -d docs/_build/doctrees docs docs/_build/html From a09e245fc22b58fa2013e56ae55576a8aee11117 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Wed, 13 Aug 2025 15:57:46 +0100 Subject: [PATCH 24/34] Revert "Modernize pyproject + tox (#1227)" (#1232) This reverts commit 3f36d499b9c11036fa10d257cbdb719f3041938c. --- pyproject.toml | 4 +--- tox.ini | 8 ++++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 56b3cf2e..883143af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,8 +40,7 @@ classifiers = [ dependencies = [ "pytest>=7.0.0", ] - -[project.optional-dependencies] +[dependency-groups] docs = [ "sphinx", "sphinx_rtd_theme", @@ -69,7 +68,6 @@ linting = [ "ruff==0.9.5", "zizmor==1.9.0", ] - [project.urls] Documentation = "https://pytest-django.readthedocs.io/" Repository = "https://github.com/pytest-dev/pytest-django" diff --git a/tox.ini b/tox.ini index e8d739e6..1caaf78d 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ envlist = linting [testenv] -extras = +dependency_groups = testing coverage: coverage mysql: mysql @@ -43,7 +43,7 @@ commands = coverage: coverage xml [testenv:linting] -extras = linting +dependency_groups = linting commands = ruff check --diff {posargs:pytest_django pytest_django_test tests} ruff format --quiet --diff {posargs:pytest_django pytest_django_test tests} @@ -54,12 +54,12 @@ commands = [testenv:doc8] basepython = python3 skip_install = true -extras = docs +dependency_groups = docs deps = doc8 commands = doc8 docs/ [testenv:docs] -extras = docs +dependency_groups = docs commands = sphinx-build -n -W -b html -d docs/_build/doctrees docs docs/_build/html From 27e4f43df6d9934e09aed56e6c22c1bd7596f687 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Wed, 13 Aug 2025 16:03:10 +0100 Subject: [PATCH 25/34] Update linter/test dependencies (#1230) --- .editorconfig-checker.json | 3 ++- pyproject.toml | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.editorconfig-checker.json b/.editorconfig-checker.json index 1e9487fe..de4ce198 100644 --- a/.editorconfig-checker.json +++ b/.editorconfig-checker.json @@ -5,7 +5,8 @@ ".ruff_cache/*", "pytest_django.egg-info/*", "__pycache__/*", - "zizmor.sarif" + "zizmor.sarif", + "docs/_build/*" ], "Disable": { "MaxLineLength": true diff --git a/pyproject.toml b/pyproject.toml index 883143af..6445d226 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,16 +57,16 @@ postgres = [ "psycopg[binary]", ] mysql = [ - "mysqlclient==2.1.0", + "mysqlclient==2.2.7", ] xdist = [ "pytest-xdist", ] linting = [ "editorconfig-checker==3.2.1", - "mypy==1.15.0", - "ruff==0.9.5", - "zizmor==1.9.0", + "mypy==1.17.1", + "ruff==0.12.8", + "zizmor==1.11.0", ] [project.urls] Documentation = "https://pytest-django.readthedocs.io/" From be5a87baeeb330eae7416bb1238ce08ade9650e3 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Thu, 14 Aug 2025 12:24:17 +0100 Subject: [PATCH 26/34] Fix more local ec issues (#1235) --- .editorconfig-checker.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.editorconfig-checker.json b/.editorconfig-checker.json index de4ce198..22317f6c 100644 --- a/.editorconfig-checker.json +++ b/.editorconfig-checker.json @@ -3,6 +3,8 @@ "pytest_django/fixtures.py", ".tox/*", ".ruff_cache/*", + ".mypy_cache/*", + ".pytest_cache/*", "pytest_django.egg-info/*", "__pycache__/*", "zizmor.sarif", From 7e2542aa16517fa23b617a58adc5188e3d27ec98 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Thu, 14 Aug 2025 12:46:44 +0100 Subject: [PATCH 27/34] Better ruff rules (#1236) --- pyproject.toml | 164 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 154 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6445d226..8b53c8b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,6 +129,7 @@ exclude_lines = [ ] [tool.ruff] +# preview = true # TODO: Enable this when we have the bandwidth line-length = 99 extend-exclude = [ "pytest_django/_version.py", @@ -136,26 +137,169 @@ extend-exclude = [ [tool.ruff.lint] extend-select = [ - "B", # flake8-bugbear + "AIR", # Airflow + "ERA", # eradicate + "FAST", # FastAPI + "YTT", # flake8-2020 + "ANN", # flake8-annotations + "ASYNC", # flake8-async + "S", # flake8-bandit "BLE", # flake8-blind-except + "FBT", # flake8-boolean-trap + "B", # flake8-bugbear + "A", # flake8-builtins + "COM", # flake8-commas + "C4", # flake8-comprehensions + "CPY", # flake8-copyright "DTZ", # flake8-datetimez + "T10", # flake8-debugger + "DJ", # flake8-django + "EM", # flake8-errmsg + "EXE", # flake8-executable + "FIX", # flake8-fixme "FA", # flake8-future-annotations + "INT", # flake8-gettext + "ISC", # flake8-implicit-str-concat + "ICN", # flake8-import-conventions + "LOG", # flake8-logging "G", # flake8-logging-format - "I", # isort - "PGH", # pygrep-hooks + "INP", # flake8-no-pep420 "PIE", # flake8-pie - "PL", # pylint - "PT", # flake8-pytest-style + "T20", # flake8-print "PYI", # flake8-pyi - "RUF", # Ruff-specific rules + "PT", # flake8-pytest-style + "Q", # flake8-quotes + "RSE", # flake8-raise + "RET", # flake8-return + "SLF", # flake8-self + "SIM", # flake8-simplify "SLOT", # flake8-slots - "T10", # flake8-debugger + "TID", # flake8-tidy-imports + "TD", # flake8-todos + "TC", # flake8-type-checking + "ARG", # flake8-unused-arguments + "PTH", # flake8-use-pathlib + "FLY", # flynt + "I", # isort + "C90", # mccabe + "PD", # pandas-vet + "N", # pep8-naming + "PERF", # Perflint + "E", # pycodestyle Error + "W", # pycodestyle Warning + "DOC", # pydoclint + "D", # pydocstyle + "F", # Pyflakes + "PGH", # pygrep-hooks + "PL", # Pylint "UP", # pyupgrade - "YTT", # flake8-2020 + "FURB", # refurb + "TRY", # tryceratops + "RUF", # Ruff-specific rules ] ignore = [ - "PLR0913", # Too many arguments in function definition - "PLR2004", # Magic value used in comparison, consider replacing 3 with a constant variable + "D100", # Missing docstring in public module + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D107", # Missing docstring in __init__ + "D200", # One-line docstring should fit on one line + "D202", # No blank lines allowed after function docstring + "D203", # Class definitions that are not preceded by a blank line + "D205", # 1 blank line required between summary line and description + "D209", # Multi-line docstring closing quotes should be on a separate line + "D212", # Multi-line docstring summary should start at the first line + "D213", # Multi-line docstring summary should start at the second line + "D400", # First line should end with a period + "D401", # First line of docstring should be in imperative mood + "D404", # First word of the docstring should not be "This" + "D415", # First line should end with a period, question mark, or exclamation point + "S101", # Use of `assert` detected + + # TODO - need to fix these + "ANN001", # Missing type annotation for function argument + "ANN002", # Missing type annotation for public function + "ANN003", # Missing type annotation for public method + "ANN201", # Missing return type annotation for public function + "ANN202", # Missing return type annotation for private function + "ANN204", # Missing return type annotation for special method + "ANN401", # Dynamically typed expressions .. are disallowed + "ARG001", # Unused function argument + "ARG002", # Unused method argument + "C901", # .. is too complex + "COM812", # Trailing comma missing + "E501", # Line too long + "EM101", # Exception must not use a string literal, assign to variable first + "EM102", # Exception must not use an f-string literal, assign to variable first + "FBT001", # Boolean-typed positional argument in function definition + "FBT002", # Boolean default positional argument in function definition + "FBT003", # Boolean positional value in function call + "N802", # Function name `assertRedirects` should be lowercase + "N806", # Variable `UserModel` in function should be lowercase + "PLC0415", # `import` should be at the top-level of a file + "PLR0913", # Too many arguments in function definition + "PLR2004", # Magic value used in comparison, consider replacing .. with a constant variable + "RET504", # Unnecessary assignment to .. before `return` statement + "RET505", # Unnecessary `elif` after `return` statement + "S105", # Possible hardcoded password assigned + "SIM102", # Use a single `if` statement instead of nested `if` statements + "SIM108", # Use ternary operator .. instead of `if`-`else`-block + "SIM114", # Combine `if` branches using logical `or` operator + "SLF001", # Private member accessed + "TC002", # Move third-party import `django.contrib.messages.Message` into a type-checking block + "TC003", # Move standard library import `collections.abc.Sequence` into a type-checking block + "TRY003", # Avoid specifying long messages outside the exception class +] +[tool.ruff.lint.per-file-ignores] +"tests/*.py" = [ + "ANN", # Disable all annotations + "FIX003", # Line contains XXX, consider resolving the issue + "DJ008", # Model does not define .. method + "N801", # Class name should use CapWords convention + "N802", # Function name should be lowercase + "S", # Disable all security checks + "TD001", # Invalid TODO tag + "TD002", # Missing author in TODO + "TD003", # Missing issue link for this TODO + + # TODO - need to fix these + "ARG005", # Unused lambda argument + "D300", # Use triple double quotes `"""` + "D403", # First word of the docstring should be capitalized + "ERA001", # Found commented-out code + "SIM117", # Use a single `with` statement with multiple contexts instead of nested `with` statements + "TC001", # Move application import .. into a type-checking block + "TC006", # Add quotes to type expression in `typing.cast()` + "PTH108", # `os.unlink()` should be replaced by `Path.unlink()` + "PTH110", # `os.path.exists()` should be replaced by `Path.exists()` + "RET503", # Missing explicit `return` at the end of function able to return non-`None` value + "RSE102", # Unnecessary parentheses on raised exception +] +"pytest_django_test/*.py" = [ + "ANN", # Disable all annotations + "FIX003", # Line contains XXX, consider resolving the issue + "DJ008", # Model does not define .. method + "N801", # Class name should use CapWords convention + "N802", # Function name should be lowercase + "S", # Disable all security checks + "TD001", # Invalid TODO tag + "TD002", # Missing author in TODO + "TD003", # Missing issue link for this TODO + + # TODO - need to fix these + "ARG005", # Unused lambda argument + "D300", # Use triple double quotes `"""` + "D403", # First word of the docstring should be capitalized + "ERA001", # Found commented-out code + "SIM117", # Use a single `with` statement with multiple contexts instead of nested `with` statements + "TC001", # Move application import .. into a type-checking block + "TC006", # Add quotes to type expression in `typing.cast()` + "PTH108", # `os.unlink()` should be replaced by `Path.unlink()` + "PTH110", # `os.path.exists()` should be replaced by `Path.exists()` + "RET503", # Missing explicit `return` at the end of function able to return non-`None` value + "RSE102", # Unnecessary parentheses on raised exception ] [tool.ruff.lint.isort] From cc73b3b166cafa81b61440a59a06d4b8de00dc05 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Fri, 15 Aug 2025 09:20:16 +0100 Subject: [PATCH 28/34] ruff: Removes all the ANN type ignores and fixes them (#1237) --- pyproject.toml | 46 +++++++++------------ pytest_django/asserts.py | 64 ++++++++++++++++++----------- pytest_django/django_compat.py | 14 +++++++ pytest_django/fixtures.py | 38 +++++++++-------- pytest_django/live_server_helper.py | 2 +- pytest_django/plugin.py | 12 +++--- tests/test_fixtures.py | 16 +++++--- 7 files changed, 113 insertions(+), 79 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8b53c8b9..61a51a2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -198,34 +198,28 @@ extend-select = [ "RUF", # Ruff-specific rules ] ignore = [ - "D100", # Missing docstring in public module - "D101", # Missing docstring in public class - "D102", # Missing docstring in public method - "D103", # Missing docstring in public function - "D104", # Missing docstring in public package - "D105", # Missing docstring in magic method - "D107", # Missing docstring in __init__ - "D200", # One-line docstring should fit on one line - "D202", # No blank lines allowed after function docstring - "D203", # Class definitions that are not preceded by a blank line - "D205", # 1 blank line required between summary line and description - "D209", # Multi-line docstring closing quotes should be on a separate line - "D212", # Multi-line docstring summary should start at the first line - "D213", # Multi-line docstring summary should start at the second line - "D400", # First line should end with a period - "D401", # First line of docstring should be in imperative mood - "D404", # First word of the docstring should not be "This" - "D415", # First line should end with a period, question mark, or exclamation point - "S101", # Use of `assert` detected + "ANN401", # Dynamically typed expressions (typing.Any) are disallowed + "D100", # Missing docstring in public module + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D107", # Missing docstring in __init__ + "D200", # One-line docstring should fit on one line + "D202", # No blank lines allowed after function docstring + "D203", # Class definitions that are not preceded by a blank line + "D205", # 1 blank line required between summary line and description + "D209", # Multi-line docstring closing quotes should be on a separate line + "D212", # Multi-line docstring summary should start at the first line + "D213", # Multi-line docstring summary should start at the second line + "D400", # First line should end with a period + "D401", # First line of docstring should be in imperative mood + "D404", # First word of the docstring should not be "This" + "D415", # First line should end with a period, question mark, or exclamation point + "S101", # Use of `assert` detected # TODO - need to fix these - "ANN001", # Missing type annotation for function argument - "ANN002", # Missing type annotation for public function - "ANN003", # Missing type annotation for public method - "ANN201", # Missing return type annotation for public function - "ANN202", # Missing return type annotation for private function - "ANN204", # Missing return type annotation for special method - "ANN401", # Dynamically typed expressions .. are disallowed "ARG001", # Unused function argument "ARG002", # Unused method argument "C901", # .. is too complex diff --git a/pytest_django/asserts.py b/pytest_django/asserts.py index f4e71dab..76a45809 100644 --- a/pytest_django/asserts.py +++ b/pytest_django/asserts.py @@ -4,7 +4,6 @@ from __future__ import annotations -from collections.abc import Sequence from functools import wraps from typing import TYPE_CHECKING, Any, Callable @@ -26,11 +25,11 @@ class MessagesTestCase(MessagesTestMixin, TestCase): test_case = TestCase("run") -def _wrapper(name: str): +def _wrapper(name: str) -> Callable[..., Any]: func = getattr(test_case, name) @wraps(func) - def assertion_func(*args, **kwargs): + def assertion_func(*args: Any, **kwargs: Any) -> Any: return func(*args, **kwargs) return assertion_func @@ -56,7 +55,12 @@ def assertion_func(*args, **kwargs): if TYPE_CHECKING: + from collections.abc import Collection, Iterator, Sequence + from contextlib import AbstractContextManager + from typing import overload + from django import forms + from django.db.models import Model, QuerySet, RawQuerySet from django.http.response import HttpResponseBase def assertRedirects( @@ -111,34 +115,34 @@ def assertTemplateUsed( template_name: str | None = ..., msg_prefix: str = ..., count: int | None = ..., - ): ... + ) -> None: ... def assertTemplateNotUsed( response: HttpResponseBase | str | None = ..., template_name: str | None = ..., msg_prefix: str = ..., - ): ... + ) -> None: ... def assertRaisesMessage( expected_exception: type[Exception], expected_message: str, - *args, - **kwargs, - ): ... + *args: Any, + **kwargs: Any, + ) -> None: ... def assertWarnsMessage( expected_warning: Warning, expected_message: str, - *args, - **kwargs, - ): ... + *args: Any, + **kwargs: Any, + ) -> None: ... def assertFieldOutput( - fieldclass, - valid, - invalid, - field_args=..., - field_kwargs=..., + fieldclass: type[forms.Field], + valid: Any, + invalid: Any, + field_args: Any = ..., + field_kwargs: Any = ..., empty_value: str = ..., ) -> None: ... @@ -194,34 +198,44 @@ def assertXMLNotEqual( # Removed in Django 5.1: use assertQuerySetEqual. def assertQuerysetEqual( - qs, - values, - transform=..., + qs: Iterator[Any] | list[Model] | QuerySet | RawQuerySet, + values: Collection[Any], + transform: Callable[[Model], Any] | type[str] | None = ..., ordered: bool = ..., msg: str | None = ..., ) -> None: ... def assertQuerySetEqual( - qs, - values, - transform=..., + qs: Iterator[Any] | list[Model] | QuerySet | RawQuerySet, + values: Collection[Any], + transform: Callable[[Model], Any] | type[str] | None = ..., ordered: bool = ..., msg: str | None = ..., ) -> None: ... + @overload + def assertNumQueries( + num: int, func: None = None, *, using: str = ... + ) -> AbstractContextManager[None]: ... + + @overload + def assertNumQueries( + num: int, func: Callable[..., Any], *args: Any, using: str = ..., **kwargs: Any + ) -> None: ... + def assertNumQueries( num: int, func=..., - *args, + *args: Any, using: str = ..., - **kwargs, + **kwargs: Any, ): ... # Added in Django 5.0. def assertMessages( response: HttpResponseBase, expected_messages: Sequence[Message], - *args, + *args: Any, ordered: bool = ..., ) -> None: ... diff --git a/pytest_django/django_compat.py b/pytest_django/django_compat.py index 6c877130..301114a8 100644 --- a/pytest_django/django_compat.py +++ b/pytest_django/django_compat.py @@ -2,9 +2,23 @@ # this is the case before you call them. from __future__ import annotations +from typing import TYPE_CHECKING + import pytest +if TYPE_CHECKING: + from typing import TypeAlias + + from django.contrib.auth.models import AbstractBaseUser + + _User: TypeAlias = AbstractBaseUser + + _UserModel: TypeAlias = type[_User] + + __all__ = ("_User", "_UserModel") + + def is_django_unittest(request_or_item: pytest.FixtureRequest | pytest.Item) -> bool: """Returns whether the request or item is a Django test case.""" from django.test import SimpleTestCase diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 115dc4cc..1dfa4c3f 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -6,7 +6,7 @@ from collections.abc import Generator, Iterable, Sequence from contextlib import AbstractContextManager, contextmanager from functools import partial -from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, Protocol, Union +from typing import TYPE_CHECKING, Protocol import pytest @@ -16,16 +16,18 @@ if TYPE_CHECKING: + from typing import Any, Callable, Literal, Optional, Union + import django import django.test from . import DjangoDbBlocker + from .django_compat import _User, _UserModel - -_DjangoDbDatabases = Optional[Union[Literal["__all__"], Iterable[str]]] -_DjangoDbAvailableApps = Optional[list[str]] -# transaction, reset_sequences, databases, serialized_rollback, available_apps -_DjangoDb = tuple[bool, bool, _DjangoDbDatabases, bool, _DjangoDbAvailableApps] + _DjangoDbDatabases = Optional[Union[Literal["__all__"], Iterable[str]]] + _DjangoDbAvailableApps = Optional[list[str]] + # transaction, reset_sequences, databases, serialized_rollback, available_apps + _DjangoDb = tuple[bool, bool, _DjangoDbDatabases, bool, _DjangoDbAvailableApps] __all__ = [ @@ -337,7 +339,7 @@ def __getitem__(self, item: str) -> None: settings.MIGRATION_MODULES = DisableMigrations() class MigrateSilentCommand(migrate.Command): - def handle(self, *args, **kwargs): + def handle(self, *args: Any, **kwargs: Any) -> Any: kwargs["verbosity"] = 0 return super().handle(*args, **kwargs) @@ -456,15 +458,15 @@ def async_client() -> django.test.AsyncClient: @pytest.fixture -def django_user_model(db: None): +def django_user_model(db: None) -> _UserModel: """The class of Django's user model.""" from django.contrib.auth import get_user_model - return get_user_model() + return get_user_model() # type: ignore[no-any-return] @pytest.fixture -def django_username_field(django_user_model) -> str: +def django_username_field(django_user_model: _UserModel) -> str: """The fieldname for the username used with Django's user model.""" field: str = django_user_model.USERNAME_FIELD return field @@ -473,9 +475,9 @@ def django_username_field(django_user_model) -> str: @pytest.fixture def admin_user( db: None, - django_user_model, + django_user_model: _User, django_username_field: str, -): +) -> _User: """A Django admin user. This uses an existing user with username "admin", or creates a new one with @@ -504,7 +506,7 @@ def admin_user( @pytest.fixture def admin_client( db: None, - admin_user, + admin_user: _User, ) -> django.test.Client: """A Django test client logged in as an admin user.""" from django.test import Client @@ -550,14 +552,14 @@ def __delattr__(self, attr: str) -> None: self._to_restore.append(override) - def __setattr__(self, attr: str, value) -> None: + def __setattr__(self, attr: str, value: Any) -> None: from django.test import override_settings override = override_settings(**{attr: value}) override.enable() self._to_restore.append(override) - def __getattr__(self, attr: str): + def __getattr__(self, attr: str) -> Any: from django.conf import settings return getattr(settings, attr) @@ -570,7 +572,7 @@ def finalize(self) -> None: @pytest.fixture -def settings(): +def settings() -> Generator[SettingsWrapper, None, None]: """A Django settings object which restores changes after the testrun""" skip_if_no_django() @@ -580,7 +582,9 @@ def settings(): @pytest.fixture(scope="session") -def live_server(request: pytest.FixtureRequest): +def live_server( + request: pytest.FixtureRequest, +) -> Generator[live_server_helper.LiveServer, None, None]: """Run a live Django server in the background during tests The address the server is started from is taken from the diff --git a/pytest_django/live_server_helper.py b/pytest_django/live_server_helper.py index 03b92e1f..e43b7e7b 100644 --- a/pytest_django/live_server_helper.py +++ b/pytest_django/live_server_helper.py @@ -84,7 +84,7 @@ def url(https://github.com/self) -> str: def __str__(self) -> str: return self.url - def __add__(self, other) -> str: + def __add__(self, other: str) -> str: return f"{self}{other}" def __repr__(self) -> str: diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 9bab8971..0c582403 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -15,7 +15,7 @@ from collections.abc import Generator from contextlib import AbstractContextManager from functools import reduce -from typing import TYPE_CHECKING, NoReturn +from typing import TYPE_CHECKING import pytest @@ -54,6 +54,8 @@ if TYPE_CHECKING: + from typing import Any, NoReturn + import django @@ -186,7 +188,7 @@ def _handle_import_error(extra_message: str) -> Generator[None, None, None]: raise ImportError(msg) from None -def _add_django_project_to_path(args) -> str: +def _add_django_project_to_path(args: list[str]) -> str: def is_django_project(path: pathlib.Path) -> bool: try: return path.is_dir() and (path / "manage.py").exists() @@ -198,7 +200,7 @@ def arg_to_path(arg: str) -> pathlib.Path: arg = arg.split("::", 1)[0] return pathlib.Path(arg) - def find_django_path(args) -> pathlib.Path | None: + def find_django_path(args: list[str]) -> pathlib.Path | None: str_args = (str(arg) for arg in args) path_args = [arg_to_path(x) for x in str_args if not x.startswith("-")] @@ -571,7 +573,7 @@ def _django_setup_unittest( original_runtest = TestCaseFunction.runtest - def non_debugging_runtest(self) -> None: + def non_debugging_runtest(self) -> None: # noqa: ANN001 self._testcase(result=self) from django.test import SimpleTestCase @@ -831,7 +833,7 @@ def _dj_db_wrapper(self) -> django.db.backends.base.base.BaseDatabaseWrapper: def _save_active_wrapper(self) -> None: self._history.append(self._dj_db_wrapper.ensure_connection) - def _blocking_wrapper(*args, **kwargs) -> NoReturn: + def _blocking_wrapper(*args: Any, **kwargs: Any) -> NoReturn: __tracebackhide__ = True raise RuntimeError( "Database access not allowed, " diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 80578959..6cb6c221 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -4,10 +4,13 @@ fixtures are tested in test_database. """ +from __future__ import annotations + import os import socket from collections.abc import Generator from contextlib import contextmanager +from typing import TYPE_CHECKING from urllib.error import HTTPError from urllib.request import urlopen @@ -25,6 +28,10 @@ from pytest_django_test.app.models import Item +if TYPE_CHECKING: + from pytest_django.django_compat import _User, _UserModel + + @contextmanager def nonverbose_config(config: pytest.Config) -> Generator[None, None, None]: """Ensure that pytest's config.option.verbose is <= 0.""" @@ -52,7 +59,7 @@ def test_admin_client(admin_client: Client) -> None: assert force_str(resp.content) == "You are an admin" -def test_admin_client_no_db_marker(admin_client: Client) -> None: +def test_admin_client_no_db_marker(db: None, admin_client: Client) -> None: assert isinstance(admin_client, Client) resp = admin_client.get("/admin-required/") assert force_str(resp.content) == "You are an admin" @@ -60,14 +67,13 @@ def test_admin_client_no_db_marker(admin_client: Client) -> None: # For test below. @pytest.fixture -def existing_admin_user(django_user_model): +def existing_admin_user(django_user_model: _UserModel) -> _User: return django_user_model._default_manager.create_superuser("admin", None, None) +@pytest.mark.django_db +@pytest.mark.usefixtures("existing_admin_user", "admin_user") def test_admin_client_existing_user( - db: None, - existing_admin_user, - admin_user, admin_client: Client, ) -> None: resp = admin_client.get("/admin-required/") From 85ed92be282c440d0233ecb04daa02ef55dcef74 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Fri, 15 Aug 2025 09:35:31 +0100 Subject: [PATCH 29/34] ruff: Fixes ruff linter to warn about all issues (#1238) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 1caaf78d..5ffeeead 100644 --- a/tox.ini +++ b/tox.ini @@ -45,7 +45,7 @@ commands = [testenv:linting] dependency_groups = linting commands = - ruff check --diff {posargs:pytest_django pytest_django_test tests} + ruff check {posargs:pytest_django pytest_django_test tests} ruff format --quiet --diff {posargs:pytest_django pytest_django_test tests} mypy {posargs:pytest_django pytest_django_test tests} ec . From e4955041f90cbc0be4eeeff41515151784b36115 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Fri, 15 Aug 2025 11:08:17 +0100 Subject: [PATCH 30/34] ruff: Addresses issues with ARG (#1239) --- pyproject.toml | 2 -- pytest_django/fixtures.py | 18 +++++----- pytest_django/plugin.py | 4 +-- pytest_django/runner.py | 8 +++-- pytest_django_test/app/views.py | 2 +- pytest_django_test/db_router.py | 6 ++-- tests/test_database.py | 56 ++++++++++++++++-------------- tests/test_fixtures.py | 60 ++++++++++++++++++++++++--------- tests/test_initialization.py | 7 +--- 9 files changed, 97 insertions(+), 66 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 61a51a2f..75915cc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -220,8 +220,6 @@ ignore = [ "S101", # Use of `assert` detected # TODO - need to fix these - "ARG001", # Unused function argument - "ARG002", # Unused method argument "C901", # .. is too complex "COM812", # Trailing comma missing "E501", # Line too long diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 1dfa4c3f..6f7929be 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -75,15 +75,15 @@ def django_db_modify_db_settings_xdist_suffix(request: pytest.FixtureRequest) -> @pytest.fixture(scope="session") def django_db_modify_db_settings_parallel_suffix( - django_db_modify_db_settings_tox_suffix: None, - django_db_modify_db_settings_xdist_suffix: None, + django_db_modify_db_settings_tox_suffix: None, # noqa: ARG001 + django_db_modify_db_settings_xdist_suffix: None, # noqa: ARG001 ) -> None: skip_if_no_django() @pytest.fixture(scope="session") def django_db_modify_db_settings( - django_db_modify_db_settings_parallel_suffix: None, + django_db_modify_db_settings_parallel_suffix: None, # noqa: ARG001 ) -> None: """Modify db settings just before the databases are configured.""" skip_if_no_django() @@ -162,12 +162,12 @@ def _get_databases_for_setup( @pytest.fixture(scope="session") def django_db_setup( request: pytest.FixtureRequest, - django_test_environment: None, + django_test_environment: None, # noqa: ARG001 django_db_blocker: DjangoDbBlocker, django_db_use_migrations: bool, django_db_keepdb: bool, django_db_createdb: bool, - django_db_modify_db_settings: None, + django_db_modify_db_settings: None, # noqa: ARG001 ) -> Generator[None, None, None]: """Top level fixture to ensure test databases are available""" from django.test.utils import setup_databases, teardown_databases @@ -206,7 +206,7 @@ def django_db_setup( @pytest.fixture def _django_db_helper( request: pytest.FixtureRequest, - django_db_setup: None, + django_db_setup: None, # noqa: ARG001 django_db_blocker: DjangoDbBlocker, ) -> Generator[None, None, None]: if is_django_unittest(request): @@ -458,7 +458,7 @@ def async_client() -> django.test.AsyncClient: @pytest.fixture -def django_user_model(db: None) -> _UserModel: +def django_user_model(db: None) -> _UserModel: # noqa: ARG001 """The class of Django's user model.""" from django.contrib.auth import get_user_model @@ -474,7 +474,7 @@ def django_username_field(django_user_model: _UserModel) -> str: @pytest.fixture def admin_user( - db: None, + db: None, # noqa: ARG001 django_user_model: _User, django_username_field: str, ) -> _User: @@ -505,7 +505,7 @@ def admin_user( @pytest.fixture def admin_client( - db: None, + db: None, # noqa: ARG001 admin_user: _User, ) -> django.test.Client: """A Django test client logged in as an admin user.""" diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 0c582403..314fb856 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -609,7 +609,7 @@ def _dj_autoclear_mailbox() -> None: @pytest.fixture def mailoutbox( - django_mail_patch_dns: None, + django_mail_patch_dns: None, # noqa: ARG001 _dj_autoclear_mailbox: None, ) -> list[django.core.mail.EmailMessage] | None: """A clean email outbox to which Django-generated emails are sent.""" @@ -833,7 +833,7 @@ def _dj_db_wrapper(self) -> django.db.backends.base.base.BaseDatabaseWrapper: def _save_active_wrapper(self) -> None: self._history.append(self._dj_db_wrapper.ensure_connection) - def _blocking_wrapper(*args: Any, **kwargs: Any) -> NoReturn: + def _blocking_wrapper(*args: Any, **kwargs: Any) -> NoReturn: # noqa: ARG002 __tracebackhide__ = True raise RuntimeError( "Database access not allowed, " diff --git a/pytest_django/runner.py b/pytest_django/runner.py index 1b6571cc..c040b749 100644 --- a/pytest_django/runner.py +++ b/pytest_django/runner.py @@ -12,7 +12,7 @@ def __init__( verbosity: int = 1, failfast: bool = False, keepdb: bool = False, - **kwargs: Any, + **kwargs: Any, # noqa: ARG002 ) -> None: self.verbosity = verbosity self.failfast = failfast @@ -24,7 +24,11 @@ def add_arguments(cls, parser: ArgumentParser) -> None: "--keepdb", action="https://github.com/store_true", help="Preserves the test DB between runs." ) - def run_tests(self, test_labels: Iterable[str], **kwargs: Any) -> int: + def run_tests( + self, + test_labels: Iterable[str], + **kwargs: Any, # noqa: ARG002 + ) -> int: """Run pytest and return the exitcode. It translates some of Django's test command option to pytest's. diff --git a/pytest_django_test/app/views.py b/pytest_django_test/app/views.py index 053f70a9..6c15babf 100644 --- a/pytest_django_test/app/views.py +++ b/pytest_django_test/app/views.py @@ -10,5 +10,5 @@ def admin_required_view(request: HttpRequest) -> HttpResponse: return HttpResponse(Template("You are an admin").render(Context())) -def item_count(request: HttpRequest) -> HttpResponse: +def item_count(request: HttpRequest) -> HttpResponse: # noqa: ARG001 return HttpResponse(f"Item count: {Item.objects.count()}") diff --git a/pytest_django_test/db_router.py b/pytest_django_test/db_router.py index e18ae853..8383a7b5 100644 --- a/pytest_django_test/db_router.py +++ b/pytest_django_test/db_router.py @@ -1,14 +1,14 @@ class DbRouter: - def db_for_read(self, model, **hints): + def db_for_read(self, model, **hints): # noqa: ARG002 if model._meta.app_label == "app" and model._meta.model_name == "seconditem": return "second" return None - def db_for_write(self, model, **hints): + def db_for_write(self, model, **hints): # noqa: ARG002 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): + def allow_migrate(self, db, app_label, model_name=None, **hints): # noqa: ARG002 if app_label == "app" and model_name == "seconditem": return db == "second" diff --git a/tests/test_database.py b/tests/test_database.py index 2fec1352..cb5b54a0 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -39,7 +39,7 @@ def test_noaccess_fixture(noaccess: None) -> None: @pytest.fixture -def non_zero_sequences_counter(db: None) -> None: +def non_zero_sequences_counter(db: None) -> None: # noqa: ARG001 """Ensure that the db's internal sequence counter is > 1. This is used to test the `reset_sequences` feature. @@ -73,20 +73,20 @@ def all_dbs(self, request: pytest.FixtureRequest) -> None: else: raise AssertionError() # pragma: no cover - def test_access(self, all_dbs: None) -> None: + def test_access(self, all_dbs: None) -> None: # noqa: ARG002 Item.objects.create(name="spam") - def test_clean_db(self, all_dbs: None) -> None: + def test_clean_db(self, all_dbs: None) -> None: # noqa: ARG002 # Relies on the order: test_access created an object assert Item.objects.count() == 0 - def test_transactions_disabled(self, db: None) -> None: + def test_transactions_disabled(self, db: None) -> None: # noqa: ARG002 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: + def test_transactions_enabled(self, transactional_db: None) -> None: # noqa: ARG002 if not connection.features.supports_transactions: pytest.skip("transactions required for this test") @@ -94,7 +94,7 @@ def test_transactions_enabled(self, transactional_db: None) -> None: def test_transactions_enabled_via_reset_seq( self, - django_db_reset_sequences: None, + django_db_reset_sequences: None, # noqa: ARG002 ) -> None: if not connection.features.supports_transactions: pytest.skip("transactions required for this test") @@ -103,9 +103,9 @@ def test_transactions_enabled_via_reset_seq( def test_django_db_reset_sequences_fixture( self, - db: None, + db: None, # noqa: ARG002 django_pytester: DjangoPytester, - non_zero_sequences_counter: None, + non_zero_sequences_counter: None, # noqa: ARG002 ) -> None: if not db_supports_reset_sequences(): pytest.skip( @@ -130,7 +130,11 @@ def test_django_db_reset_sequences_requested( result = django_pytester.runpytest_subprocess("-v", "--reuse-db") result.stdout.fnmatch_lines(["*test_django_db_reset_sequences_requested PASSED*"]) - def test_serialized_rollback(self, db: None, django_pytester: DjangoPytester) -> None: + def test_serialized_rollback( + self, + db: None, # noqa: ARG002 + django_pytester: DjangoPytester, + ) -> None: django_pytester.create_app_file( """ from django.db import migrations @@ -176,11 +180,11 @@ def test_serialized_rollback_3(): assert result.ret == 0 @pytest.fixture - def mydb(self, all_dbs: None) -> None: + def mydb(self, all_dbs: None) -> None: # noqa: ARG002 # This fixture must be able to access the database Item.objects.create(name="spam") - def test_mydb(self, mydb: None) -> None: + def test_mydb(self, mydb: None) -> None: # noqa: ARG002 if not connection.features.supports_transactions: pytest.skip("transactions required for this test") @@ -188,13 +192,13 @@ def test_mydb(self, mydb: None) -> None: item = Item.objects.get(name="spam") assert item - def test_fixture_clean(self, all_dbs: None) -> None: + def test_fixture_clean(self, all_dbs: None) -> None: # noqa: ARG002 # 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: pytest.FixtureRequest, all_dbs: None) -> Generator[None, None, None]: + def fin(self, all_dbs: None) -> Generator[None, None, None]: # noqa: ARG002 # This finalizer must be able to access the database yield Item.objects.create(name="spam") @@ -203,7 +207,7 @@ def test_fin(self, fin: None) -> None: # Check finalizer has db access (teardown will fail if not) pass - def test_durable_transactions(self, all_dbs: None) -> None: + def test_durable_transactions(self, all_dbs: None) -> None: # noqa: ARG002 with transaction.atomic(durable=True): item = Item.objects.create(name="foo") assert Item.objects.get() == item @@ -211,19 +215,19 @@ def test_durable_transactions(self, all_dbs: None) -> None: class TestDatabaseFixturesAllOrder: @pytest.fixture - def fixture_with_db(self, db: None) -> None: + def fixture_with_db(self, db: None) -> None: # noqa: ARG002 Item.objects.create(name="spam") @pytest.fixture - def fixture_with_transdb(self, transactional_db: None) -> None: + def fixture_with_transdb(self, transactional_db: None) -> None: # noqa: ARG002 Item.objects.create(name="spam") @pytest.fixture - def fixture_with_reset_sequences(self, django_db_reset_sequences: None) -> None: + def fixture_with_reset_sequences(self, django_db_reset_sequences: None) -> None: # noqa: ARG002 Item.objects.create(name="spam") @pytest.fixture - def fixture_with_serialized_rollback(self, django_db_serialized_rollback: None) -> None: + def fixture_with_serialized_rollback(self, django_db_serialized_rollback: None) -> None: # noqa: ARG002 Item.objects.create(name="ham") def test_trans(self, fixture_with_transdb: None) -> None: @@ -311,27 +315,27 @@ def test_databases(self, request: pytest.FixtureRequest) -> None: assert marker.kwargs["databases"] == ["default", "replica", "second"] @pytest.mark.django_db(databases=["second"]) - def test_second_database(self, request: pytest.FixtureRequest) -> None: + def test_second_database(self) -> None: SecondItem.objects.create(name="spam") @pytest.mark.django_db(databases=["default"]) - def test_not_allowed_database(self, request: pytest.FixtureRequest) -> None: + def test_not_allowed_database(self) -> 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: pytest.FixtureRequest) -> None: + def test_replica_database(self) -> None: Item.objects.using("replica").count() @pytest.mark.django_db(databases=["replica"]) - def test_replica_database_not_allowed(self, request: pytest.FixtureRequest) -> None: + def test_replica_database_not_allowed(self) -> 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: pytest.FixtureRequest) -> None: + def test_replica_mirrors_default_database(self) -> None: Item.objects.create(name="spam") Item.objects.using("replica").create(name="spam") @@ -339,7 +343,7 @@ def test_replica_mirrors_default_database(self, request: pytest.FixtureRequest) assert Item.objects.using("replica").count() == 2 @pytest.mark.django_db(databases="__all__") - def test_all_databases(self, request: pytest.FixtureRequest) -> None: + def test_all_databases(self) -> None: Item.objects.count() Item.objects.create(name="spam") SecondItem.objects.count() @@ -369,7 +373,7 @@ def test_available_apps_enabled(self, request: pytest.FixtureRequest) -> None: assert marker.kwargs["available_apps"] == ["pytest_django_test.app"] @pytest.mark.django_db - def test_available_apps_default(self, request: pytest.FixtureRequest) -> None: + def test_available_apps_default(self) -> None: from django.apps import apps from django.conf import settings @@ -377,7 +381,7 @@ def test_available_apps_default(self, request: pytest.FixtureRequest) -> None: assert apps.is_installed(app) @pytest.mark.django_db(available_apps=["pytest_django_test.app"]) - def test_available_apps_limited(self, request: pytest.FixtureRequest) -> None: + def test_available_apps_limited(self) -> None: from django.apps import apps from django.conf import settings diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 6cb6c221..16a548d4 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -30,6 +30,8 @@ if TYPE_CHECKING: from pytest_django.django_compat import _User, _UserModel + from pytest_django.fixtures import SettingsWrapper + from pytest_django.live_server_helper import LiveServer @contextmanager @@ -59,7 +61,10 @@ def test_admin_client(admin_client: Client) -> None: assert force_str(resp.content) == "You are an admin" -def test_admin_client_no_db_marker(db: None, admin_client: Client) -> None: +def test_admin_client_no_db_marker( + db: None, # noqa: ARG001 + admin_client: Client, +) -> None: assert isinstance(admin_client, Client) resp = admin_client.get("/admin-required/") assert force_str(resp.content) == "You are an admin" @@ -144,7 +149,7 @@ def test_django_assert_max_num_queries_db( @pytest.mark.django_db(transaction=True) def test_django_assert_num_queries_transactional_db( request: pytest.FixtureRequest, - transactional_db: None, + transactional_db: None, # noqa: ARG001 django_assert_num_queries: DjangoAssertNumQueries, ) -> None: with nonverbose_config(request.config): @@ -373,7 +378,13 @@ def test_deleted_again(self, settings) -> None: def test_signals(self, settings) -> None: result = [] - def assert_signal(signal, sender, setting, value, enter) -> None: + def assert_signal( + signal, # noqa: ARG001 + sender, # noqa: ARG001 + setting, + value, + enter, + ) -> None: result.append((setting, value, enter)) from django.test.signals import setting_changed @@ -462,10 +473,14 @@ def test_settings_before(self) -> None: ) TestLiveServer._test_settings_before_run = True # type: ignore[attr-defined] - def test_url(https://github.com/self,%20live_server) -> None: + def test_url(https://github.com/self,%20live_server:%20LiveServer) -> None: assert live_server.url == force_str(live_server) - def test_change_settings(self, live_server, settings) -> None: + def test_change_settings( + self, + live_server: LiveServer, + settings: SettingsWrapper, # noqa: ARG002 + ) -> None: assert live_server.url == force_str(live_server) @pytest.mark.skipif("PYTEST_XDIST_WORKER" in os.environ, reason="xdist in use") @@ -480,7 +495,7 @@ def test_settings_restored(self) -> None: ) assert settings.ALLOWED_HOSTS == ["testserver"] - def test_transactions(self, live_server) -> None: + def test_transactions(self, live_server: LiveServer) -> None: # noqa: ARG002 if not connection.features.supports_transactions: pytest.skip("transactions required for this test") @@ -493,12 +508,20 @@ def test_db_changes_visibility(self, live_server) -> None: response_data = urlopen(live_server + "/item_count/").read() assert force_str(response_data) == "Item count: 1" - def test_fixture_db(self, db: None, live_server) -> None: + def test_fixture_db( + self, + db: None, # noqa: ARG002 + live_server: LiveServer, + ) -> 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: None, live_server) -> None: + def test_fixture_transactional_db( + self, + transactional_db: None, # noqa: ARG002 + live_server: LiveServer, + ) -> None: Item.objects.create(name="foo") response_data = urlopen(live_server + "/item_count/").read() assert force_str(response_data) == "Item count: 1" @@ -510,24 +533,32 @@ def item(self) -> Item: item: Item = Item.objects.create(name="foo") return item - def test_item(self, item: Item, live_server) -> None: + def test_item(self, item: Item, live_server: LiveServer) -> None: pass @pytest.fixture - def item_db(self, db: None) -> Item: + def item_db(self, db: None) -> Item: # noqa: ARG002 item: Item = Item.objects.create(name="foo") return item - def test_item_db(self, item_db: Item, live_server) -> None: + def test_item_db( + self, + item_db: Item, # noqa: ARG002 + 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: None) -> Item: + def item_transactional_db(self, transactional_db: None) -> Item: # noqa: ARG002 item: Item = Item.objects.create(name="foo") return item - def test_item_transactional_db(self, item_transactional_db: Item, live_server) -> None: + def test_item_transactional_db( + self, + item_transactional_db: Item, # noqa: ARG002 + live_server: LiveServer, + ) -> None: response_data = urlopen(live_server + "/item_count/").read() assert force_str(response_data) == "Item count: 1" @@ -548,7 +579,6 @@ def test_item_transactional_db(self, item_transactional_db: Item, live_server) - def test_serve_static_with_staticfiles_app( self, django_pytester: DjangoPytester, - settings, ) -> None: """ LiveServer always serves statics with ``django.contrib.staticfiles`` @@ -573,7 +603,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) -> None: + def test_serve_static_dj17_without_staticfiles_app(self, live_server) -> None: """ Because ``django.contrib.staticfiles`` is not installed LiveServer can not serve statics with django >= 1.7 . diff --git a/tests/test_initialization.py b/tests/test_initialization.py index a15b9f9a..631a41ed 100644 --- a/tests/test_initialization.py +++ b/tests/test_initialization.py @@ -1,14 +1,9 @@ from textwrap import dedent -import pytest - from .helpers import DjangoPytester -def test_django_setup_order_and_uniqueness( - django_pytester: DjangoPytester, - monkeypatch: pytest.MonkeyPatch, -) -> None: +def test_django_setup_order_and_uniqueness(django_pytester: DjangoPytester) -> None: """ The django.setup() function shall not be called multiple times by pytest-django, since it resets logging conf each time. From 1e1f70f9dc58080382d11b0a9be8b4bf9a28924a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:37:01 +0200 Subject: [PATCH 31/34] build(deps): bump actions/checkout from 4 to 5 (#1242) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy.yml | 2 +- .github/workflows/main.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f06318be..8562044f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -14,7 +14,7 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b6fb5cb8..68ff5604 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,7 +29,7 @@ jobs: env: TOXENV: ${{ matrix.name }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: persist-credentials: false From ef9fef6aa1bef09054b424079e734522748ef547 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 09:41:57 +0000 Subject: [PATCH 32/34] build(deps): bump actions/download-artifact from 4 to 5 (#1241) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Javier Buzzi --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8562044f..a713369b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -34,7 +34,7 @@ jobs: steps: - name: Download Package - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: Packages path: dist From 397ca28f07a35671cf9003720238fd4422dcc280 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 11:25:38 +0200 Subject: [PATCH 33/34] build(deps): bump pypa/gh-action-pypi-publish from 1.12.4 to 1.13.0 (#1245) Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.12.4 to 1.13.0. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/76f52bc884231f62b9a034ebfe128415bbaabdfc...ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-version: 1.13.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a713369b..5a591524 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -40,4 +40,4 @@ jobs: path: dist - name: Publish package - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 From 72b13b5b3a782bb7fa1c57162922d665a588f681 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 11:26:07 +0200 Subject: [PATCH 34/34] build(deps): bump actions/setup-python from 5 to 6 (#1244) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .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 68ff5604..65501027 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -33,7 +33,7 @@ jobs: with: persist-credentials: false - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }}