diff options
| author | Baptiste Mispelon <bmispelon@gmail.com> | 2021-12-10 12:22:23 +0100 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2022-04-06 07:58:52 +0200 |
| commit | 50e1e7ef8ef63271717f8bcab76d7151ccf4bb18 (patch) | |
| tree | 888beb2bff18679025536e574a0bc02a53f9d552 /django | |
| parent | 1a7d75cf77639e450854d9bcf9518664f755eb04 (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.py | 3 | ||||
| -rw-r--r-- | django/test/testcases.py | 259 |
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): |
