summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorBaptiste Mispelon <bmispelon@gmail.com>2021-12-10 12:22:23 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2022-04-06 07:58:52 +0200
commit50e1e7ef8ef63271717f8bcab76d7151ccf4bb18 (patch)
tree888beb2bff18679025536e574a0bc02a53f9d552 /django
parent1a7d75cf77639e450854d9bcf9518664f755eb04 (diff)
Fixed #33348 -- Changed SimpleTestCase.assertFormError()/assertFormsetErrors() to take form/formset.
Instead of taking a response object and a context name for the form/formset, the two methods now take the object directly.
Diffstat (limited to 'django')
-rw-r--r--django/contrib/admin/helpers.py3
-rw-r--r--django/test/testcases.py259
2 files changed, 170 insertions, 92 deletions
diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
index cc5317199d..9068ce57da 100644
--- a/django/contrib/admin/helpers.py
+++ b/django/contrib/admin/helpers.py
@@ -441,9 +441,8 @@ class InlineAdminFormSet:
def forms(self):
return self.formset.forms
- @property
def non_form_errors(self):
- return self.formset.non_form_errors
+ return self.formset.non_form_errors()
@property
def is_bound(self):
diff --git a/django/test/testcases.py b/django/test/testcases.py
index bfcfef58cb..53d3838bb8 100644
--- a/django/test/testcases.py
+++ b/django/test/testcases.py
@@ -1,5 +1,6 @@
import asyncio
import difflib
+import inspect
import json
import logging
import posixpath
@@ -42,6 +43,7 @@ from django.db import DEFAULT_DB_ALIAS, connection, connections, transaction
from django.forms.fields import CharField
from django.http import QueryDict
from django.http.request import split_domain_port, validate_host
+from django.http.response import HttpResponseBase
from django.test.client import AsyncClient, Client
from django.test.html import HTMLParseError, parse_html
from django.test.signals import template_rendered
@@ -165,6 +167,127 @@ class _DatabaseFailure:
raise DatabaseOperationForbidden(self.message)
+# RemovedInDjango50Warning
+class _AssertFormErrorDeprecationHelper:
+ @staticmethod
+ def assertFormError(self, response, form, field, errors, msg_prefix=""):
+ """
+ Search through all the rendered contexts of the `response` for a form named
+ `form` then dispatch to the new assertFormError() using that instance.
+ If multiple contexts contain the form, they're all checked in order and any
+ failure will abort (this matches the old behavior).
+ """
+ warning_msg = (
+ f"Passing response to assertFormError() is deprecated. Use the form object "
+ f"directly: assertFormError(response.context[{form!r}], {field!r}, ...)"
+ )
+ warnings.warn(warning_msg, RemovedInDjango50Warning, stacklevel=2)
+
+ full_msg_prefix = f"{msg_prefix}: " if msg_prefix else ""
+ contexts = to_list(response.context) if response.context is not None else []
+ if not contexts:
+ self.fail(
+ f"{full_msg_prefix}Response did not use any contexts to render the "
+ f"response"
+ )
+ # Search all contexts for the error.
+ found_form = False
+ for i, context in enumerate(contexts):
+ if form not in context:
+ continue
+ found_form = True
+ self.assertFormError(context[form], field, errors, msg_prefix=msg_prefix)
+ if not found_form:
+ self.fail(
+ f"{full_msg_prefix}The form '{form}' was not used to render the "
+ f"response"
+ )
+
+ @staticmethod
+ def assertFormsetError(
+ self, response, formset, form_index, field, errors, msg_prefix=""
+ ):
+ """
+ Search for a formset named "formset" in the "response" and dispatch to
+ the new assertFormsetError() using that instance. If the name is found
+ in multiple contexts they're all checked in order and any failure will
+ abort the test.
+ """
+ warning_msg = (
+ f"Passing response to assertFormsetError() is deprecated. Use the formset "
+ f"object directly: assertFormsetError(response.context[{formset!r}], "
+ f"{form_index!r}, ...)"
+ )
+ warnings.warn(warning_msg, RemovedInDjango50Warning, stacklevel=2)
+
+ full_msg_prefix = f"{msg_prefix}: " if msg_prefix else ""
+ contexts = to_list(response.context) if response.context is not None else []
+ if not contexts:
+ self.fail(
+ f"{full_msg_prefix}Response did not use any contexts to render the "
+ f"response"
+ )
+ found_formset = False
+ for i, context in enumerate(contexts):
+ if formset not in context or not hasattr(context[formset], "forms"):
+ continue
+ found_formset = True
+ self.assertFormsetError(
+ context[formset], form_index, field, errors, msg_prefix
+ )
+ if not found_formset:
+ self.fail(
+ f"{full_msg_prefix}The formset '{formset}' was not used to render the "
+ f"response"
+ )
+
+ @classmethod
+ def patch_signature(cls, new_method):
+ """
+ Replace the decorated method with a new one that inspects the passed
+ args/kwargs and dispatch to the old implementation (with deprecation
+ warning) when it detects the old signature.
+ """
+
+ @wraps(new_method)
+ def patched_method(self, *args, **kwargs):
+ old_method = getattr(cls, new_method.__name__)
+ old_signature = inspect.signature(old_method)
+ try:
+ old_bound_args = old_signature.bind(self, *args, **kwargs)
+ except TypeError:
+ # If old signature doesn't match then either:
+ # 1) new signature will match
+ # 2) or a TypeError will be raised showing the user information
+ # about the new signature.
+ return new_method(self, *args, **kwargs)
+
+ new_signature = inspect.signature(new_method)
+ try:
+ new_bound_args = new_signature.bind(self, *args, **kwargs)
+ except TypeError:
+ # Old signature matches but not the new one (because of
+ # previous try/except).
+ return old_method(self, *args, **kwargs)
+
+ # If both signatures match, decide on which method to call by
+ # inspecting the first arg (arg[0] = self).
+ assert old_bound_args.args[1] == new_bound_args.args[1]
+ if hasattr(
+ old_bound_args.args[1], "context"
+ ): # Looks like a response object => old method.
+ return old_method(self, *args, **kwargs)
+ elif isinstance(old_bound_args.args[1], HttpResponseBase):
+ raise ValueError(
+ f"{old_method.__name__}() is only usable on responses fetched "
+ f"using the Django test Client."
+ )
+ else:
+ return new_method(self, *args, **kwargs)
+
+ return patched_method
+
+
class SimpleTestCase(unittest.TestCase):
# The class we'll use for the test client self.client.
@@ -585,22 +708,19 @@ class SimpleTestCase(unittest.TestCase):
self.assertEqual(field_errors, errors, msg_prefix + failure_message)
- def assertFormError(self, response, form, field, errors, msg_prefix=""):
- """
- Assert that a form used to render the response has a specific field
- error.
+ # RemovedInDjango50Warning: When the deprecation ends, remove the
+ # decorator.
+ @_AssertFormErrorDeprecationHelper.patch_signature
+ def assertFormError(self, form, field, errors, msg_prefix=""):
"""
- self._check_test_client_response(response, "context", "assertFormError")
- if msg_prefix:
- msg_prefix += ": "
+ Assert that a field named "field" on the given form object has specific
+ errors.
- # Put context(s) into a list to simplify processing.
- contexts = [] if response.context is None else to_list(response.context)
- if not contexts:
- self.fail(
- msg_prefix + "Response did not use any contexts to render the response"
- )
+ errors can be either a single error message or a list of errors
+ messages. Using errors=[] test that the field has no errors.
+ You can pass field=None to check the form's non-field errors.
+ """
if errors is None:
warnings.warn(
"Passing errors=None to assertFormError() is deprecated, use "
@@ -609,47 +729,25 @@ class SimpleTestCase(unittest.TestCase):
stacklevel=2,
)
errors = []
- # Put error(s) into a list to simplify processing.
- errors = to_list(errors)
-
- # Search all contexts for the error.
- found_form = False
- for i, context in enumerate(contexts):
- if form in context:
- found_form = True
- self._assert_form_error(
- context[form], field, errors, msg_prefix, "form %r" % context[form]
- )
- if not found_form:
- self.fail(
- msg_prefix + "The form '%s' was not used to render the response" % form
- )
-
- def assertFormsetError(
- self, response, formset, form_index, field, errors, msg_prefix=""
- ):
- """
- Assert that a formset used to render the response has a specific error.
- For field errors, specify the ``form_index`` and the ``field``.
- For non-field errors, specify the ``form_index`` and the ``field`` as
- None.
- For non-form errors, specify ``form_index`` as None and the ``field``
- as None.
- """
- self._check_test_client_response(response, "context", "assertFormsetError")
- # Add punctuation to msg_prefix
if msg_prefix:
msg_prefix += ": "
+ errors = to_list(errors)
+ self._assert_form_error(form, field, errors, msg_prefix, f"form {form!r}")
- # Put context(s) into a list to simplify processing.
- contexts = [] if response.context is None else to_list(response.context)
- if not contexts:
- self.fail(
- msg_prefix + "Response did not use any contexts to "
- "render the response"
- )
+ # RemovedInDjango50Warning: When the deprecation ends, remove the
+ # decorator.
+ @_AssertFormErrorDeprecationHelper.patch_signature
+ def assertFormsetError(self, formset, form_index, field, errors, msg_prefix=""):
+ """
+ Similar to assertFormError() but for formsets.
+ Use form_index=None to check the formset's non-form errors (in that
+ case, you must also use field=None).
+ Otherwise use an integer to check the formset's n-th form for errors.
+
+ Other parameters are the same as assertFormError().
+ """
if errors is None:
warnings.warn(
"Passing errors=None to assertFormsetError() is deprecated, "
@@ -662,50 +760,31 @@ class SimpleTestCase(unittest.TestCase):
if form_index is None and field is not None:
raise ValueError("You must use field=None with form_index=None.")
- # Put error(s) into a list to simplify processing.
+ if msg_prefix:
+ msg_prefix += ": "
errors = to_list(errors)
- # Search all contexts for the error.
- found_formset = False
- for i, context in enumerate(contexts):
- if formset not in context or not hasattr(context[formset], "forms"):
- continue
- formset_repr = repr(context[formset])
- if not context[formset].is_bound:
- self.fail(
- f"{msg_prefix}The formset {formset_repr} is not bound, it will "
- f"never have any errors."
- )
- found_formset = True
- if form_index is not None:
- form_count = context[formset].total_form_count()
- if form_index >= form_count:
- form_or_forms = "forms" if form_count > 1 else "form"
- self.fail(
- f"{msg_prefix}The formset {formset_repr} only has "
- f"{form_count} {form_or_forms}."
- )
- if form_index is not None:
- form_repr = f"form {form_index} of formset {formset_repr}"
- self._assert_form_error(
- context[formset].forms[form_index],
- field,
- errors,
- msg_prefix,
- form_repr,
- )
- else:
- failure_message = (
- f"{msg_prefix}The non-form errors of formset {formset_repr} don't "
- f"match."
- )
- self.assertEqual(
- context[formset].non_form_errors(), errors, failure_message
- )
- if not found_formset:
+ if not formset.is_bound:
self.fail(
- msg_prefix
- + "The formset '%s' was not used to render the response" % formset
+ f"{msg_prefix}The formset {formset!r} is not bound, it will never have "
+ f"any errors."
+ )
+ if form_index is not None and form_index >= formset.total_form_count():
+ form_count = formset.total_form_count()
+ form_or_forms = "forms" if form_count > 1 else "form"
+ self.fail(
+ f"{msg_prefix}The formset {formset!r} only has {form_count} "
+ f"{form_or_forms}."
+ )
+ if form_index is not None:
+ form_repr = f"form {form_index} of formset {formset!r}"
+ self._assert_form_error(
+ formset.forms[form_index], field, errors, msg_prefix, form_repr
+ )
+ else:
+ failure_message = f"The non-form errors of formset {formset!r} don't match."
+ self.assertEqual(
+ formset.non_form_errors(), errors, msg_prefix + failure_message
)
def _get_template_used(self, response, template_name, msg_prefix, method_name):