Skip to content

Exposes the underlining TestCase instance to avoid using "asserts" module#1191

Open
kingbuzzman wants to merge 49 commits intopytest-dev:mainfrom
kingbuzzman:patch-5
Open

Exposes the underlining TestCase instance to avoid using "asserts" module#1191
kingbuzzman wants to merge 49 commits intopytest-dev:mainfrom
kingbuzzman:patch-5

Conversation

@kingbuzzman
Copy link
Member

@kingbuzzman kingbuzzman commented Apr 6, 2025

Related to issue: #1155

@kingbuzzman kingbuzzman marked this pull request as ready for review April 6, 2025 20:29
@kingbuzzman
Copy link
Member Author

@bluetech thoughts?

@bluetech
Copy link
Member

bluetech commented Apr 7, 2025

I have a (vain?) hope that someday pytest will be thread safe (i.e. specifically, it should be possible to run multiple pytest instances concurrently in the same process). Currently it isn't, and even pytest-django adds some issues itself. But I think it better to avoid adding new ones.

I think if we decide that's it worth putting effort into this issue, we should find a way to make the assert "bind" to the test it is running under. And if there's no clean way to do that, then consider an alternative interface which does make it possible (e.g. a django_assert fixture or such which binds to the current test via request and exposes the assertions as methods, e.g. django_assert.templateUsed()).

@kingbuzzman
Copy link
Member Author

But I think it better to avoid adding new ones.

This makes sense.

find a way to make the assert "bind" to the test it is running under.

I have some ideas on how to make this work

@kingbuzzman kingbuzzman marked this pull request as draft April 7, 2025 10:39
@kingbuzzman kingbuzzman changed the title Fixes issue with maxDiff and verbosity Exposes the underlining TestCase instance to avoid using "asserts" module Apr 7, 2025
@kingbuzzman
Copy link
Member Author

@bluetech what do you think of this approach?

@kingbuzzman kingbuzzman requested review from adamchainz and Copilot and removed request for Copilot April 7, 2025 13:26
@kingbuzzman kingbuzzman requested a review from Copilot April 9, 2025 14:12
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 4 out of 5 changed files in this pull request and generated no comments.

Files not reviewed (1)
  • docs/helpers.rst: Language not supported
Comments suppressed due to low confidence (2)

pytest_django/fixtures.py:221

  • [nitpick] The fixture name 'django_testcase_class' might be ambiguous; consider renaming it to 'DjangoTestCaseClass' to clearly indicate that it returns a TestCase class.
def django_testcase_class(

pytest_django/asserts.py:34

  • [nitpick] The deprecation warning message could be made clearer by explicitly specifying the recommended usage of the 'django_testcase' fixture along with its attributes.
message = (

@kingbuzzman
Copy link
Member Author

@bluetech thoughts how I can improve this?

@bluetech
Copy link
Member

bluetech commented Jun 2, 2025

@kingbuzzman That's a creative solution, I like it! I will definitely try to review this before the next release.

@adamchainz
Copy link
Member

I think the fixture will make tests too verbose, and the deprecation will cause unnecessary churn. I think it would be better with a shorter fixture name and a retention of the asserts module. Perhaps the asserts module can be rewritten with wrapper functions that call the fixture methods?

@kingbuzzman
Copy link
Member Author

kingbuzzman commented Jun 3, 2025

@adamchainz understand the making the fixture name smaller, but not sure about how the wrapper would work without frame hacking.. can you provide an example?

@kingbuzzman
Copy link
Member Author

What do you guys think of renaming "django_testcase" to just djtc it's 4 characters, similar to "self"? I'm open for suggestions

@adamchainz
Copy link
Member

ow sure about how the wrapper would work without frame hacking.. can you provide an example?

Sorry, no, I can't. I hadn't grasped the whole problem there.

What do you guys think of renaming "django_testcase" to just djtc it's 4 characters, similar to "self"? I'm open for suggestions

Maybe just djt...?

@kingbuzzman
Copy link
Member Author

djt

i'm good with this

the only other thing is remove the warnings, so we don't cause developer stress? i mean, i don't disagree, but i would also love to get rid of this file in say 5 versions from now, or 10... i don't care about this arbitrary number. what is a good plan to deprecate this?

@kingbuzzman
Copy link
Member Author

@bluetech thoughts?

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +291 to +307
def _django_db_helper(
request: pytest.FixtureRequest,
django_db_setup: None, # noqa: ARG001
django_db_blocker: DjangoDbBlocker,
django_testcase_class: type[django.test.TestCase],
) -> Generator[None, None, None]:
if is_django_unittest(request):
yield
return

with django_db_blocker.unblock():
import django.db
import django.test
django_testcase_class.setUpClass()

if transactional:
test_case_class = django.test.TransactionTestCase
else:
test_case_class = django.test.TestCase

_reset_sequences = reset_sequences
_serialized_rollback = serialized_rollback
_databases = databases
_available_apps = available_apps

class PytestDjangoTestCase(test_case_class): # type: ignore[misc,valid-type]
reset_sequences = _reset_sequences
serialized_rollback = _serialized_rollback
if _databases is not None:
databases = _databases
if _available_apps is not None:
available_apps = _available_apps

# For non-transactional tests, skip executing `django.test.TestCase`'s
# `setUpClass`/`tearDownClass`, only execute the super class ones.
#
# `TestCase`'s class setup manages the `setUpTestData`/class-level
# transaction functionality. We don't use it; instead we (will) offer
# our own alternatives. So it only adds overhead, and does some things
# which conflict with our (planned) functionality, particularly, it
# closes all database connections in `tearDownClass` which inhibits
# wrapping tests in higher-scoped transactions.
#
# It's possible a new version of Django will add some unrelated
# functionality to these methods, in which case skipping them completely
# would not be desirable. Let's cross that bridge when we get there...
if not transactional:

@classmethod
def setUpClass(cls) -> None:
super(django.test.TestCase, cls).setUpClass()

@classmethod
def tearDownClass(cls) -> None:
super(django.test.TestCase, cls).tearDownClass()

PytestDjangoTestCase.setUpClass()

test_case = PytestDjangoTestCase(methodName="__init__")
test_case = django_testcase_class(methodName="__init__")
test_case._pre_setup()

yield
yield test_case
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_django_db_helper is annotated as Generator[None, None, None] but it now yields test_case (a TestCase/TransactionTestCase instance). Update the generator yield type (and any dependent fixture parameter annotations) so the typing matches the runtime behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +295 to +300
django_testcase_class: type[django.test.TestCase],
) -> Generator[None, None, None]:
if is_django_unittest(request):
yield
return

Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_django_db_helper declares django_testcase_class: type[django.test.TestCase], but django_testcase_class can yield None for Django unittest items. Even though the function returns early in that branch, the injected value is still None, so the annotation should be optional (and then narrowed after the is_django_unittest check) to avoid type errors.

Suggested change
django_testcase_class: type[django.test.TestCase],
) -> Generator[None, None, None]:
if is_django_unittest(request):
yield
return
django_testcase_class: type[django.test.TestCase] | None,
) -> Generator[None, None, None]:
if is_django_unittest(request):
yield
return
assert django_testcase_class is not None

Copilot uses AI. Check for mistakes.
Comment on lines 388 to 390
@pytest.fixture
def db(_django_db_helper: None) -> None:
"""Require a django test database.
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

db (and similar fixtures) annotate _django_db_helper as None, but _django_db_helper now yields a TestCase instance. Even if unused, this annotation is now incorrect and will confuse type checkers; update it to the actual yielded type (or a common base/Protocol) for consistency.

Copilot uses AI. Check for mistakes.
docs/helpers.rst Outdated
::

from pytest_django.asserts import assertTemplateUsed
:ref:`django:assertions` are available in via the :fixture:`djt` fixture.
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grammar: "are available in via" should be "are available via" (or "are available through").

Suggested change
:ref:`django:assertions` are available in via the :fixture:`djt` fixture.
:ref:`django:assertions` are available via the :fixture:`djt` fixture.

Copilot uses AI. Check for mistakes.
Comment on lines +82 to +85
with pytest.raises(AssertionError):
djt.assertXMLEqual("a" * 10_000, "a")


Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new fixture tests exercise djt.assertXMLEqual with a long string, but they don't verify the motivation from issue #1155 (that callers can set djt.maxDiff = None and get an untruncated diff). Consider adding an assertion that demonstrates maxDiff can be changed on the provided instance and that it affects the failure output.

Suggested change
with pytest.raises(AssertionError):
djt.assertXMLEqual("a" * 10_000, "a")
djt.maxDiff = None
with pytest.raises(AssertionError) as excinfo:
djt.assertXMLEqual("a" * 10_000, "a")
assert "a" * 10_000 in str(excinfo.value)

Copilot uses AI. Check for mistakes.
django_db_setup: None, # noqa: ARG001
django_db_blocker: DjangoDbBlocker,
) -> Generator[None, None, None]:
) -> Generator[type[django.test.TestCase] | None, None, None]:
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

django_testcase_class can yield either django.test.TestCase or django.test.TransactionTestCase, but the return type is annotated as type[django.test.TestCase] | None. Since TransactionTestCase is not a subclass of TestCase, this is too narrow and will fail type-checking; consider typing this as type[django.test.TransactionTestCase] | None (or another common base) and keep downstream annotations consistent.

Suggested change
) -> Generator[type[django.test.TestCase] | None, None, None]:
) -> Generator[type[django.test.TransactionTestCase] | None, None, None]:

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants