#37089: ignore_warnings() handles message arg differently from other test 
helpers
-------------------------------------+-------------------------------------
     Reporter:  Mike Edmunds         |                    Owner:  Mike
                                     |  Edmunds
         Type:  Uncategorized        |                   Status:  assigned
    Component:  Uncategorized        |                  Version:  dev
     Severity:  Normal               |               Resolution:
     Keywords:  ignore_warnings,     |             Triage Stage:
  deprecation                        |  Unreviewed
    Has patch:  0                    |      Needs documentation:  0
  Needs tests:  0                    |  Patch needs improvement:  0
Easy pickings:  0                    |                    UI/UX:  0
-------------------------------------+-------------------------------------

Old description:

> This use of `ignore_warnings()` does ''not'' ignore the warning:
>
> {{{#!python
> with ignore_warnings(
>     category=RemovedInDjango70Warning,
>     message="get_connection() is deprecated.",
> ):
>     warnings.warn(
>         "get_connection() is deprecated.",
>         RemovedInDjango70Warning,
>     )
> }}}
>
> That's inconsistent with how `assertWarnsMessage()` works, and is likely
> to be unexpected.
>
> Internally, `ignore_warnings()` uses Python's
> `warnings.catch_warnings()`, which passes the `message` arg directly to
> `re.match()`. (So `ignore_warnings()` is also inconsistent with
> `assertWarnsRegex()`, which uses `re.search()`.) See the reproduction
> example below.
>
> The `message` arg is not documented, but has some specific support in
> `ignore_warnings()`. (Same thing for the `module` arg, though that's less
> of an issue since the only regex pattern char commonly in module names is
> ".", and the pattern `r"."` matches the string `"."`.)
>
> == Proposal ==
>
> We should change `ignore_warnings()` to `re.escape()` the `message`  arg
> and prepend `r".*"` to it, making it consistent with
> `assertWarnsMessage()` by default.
>
> For test cases that do want ignore messages by regex, we would introduce
> a `message_re` arg. Only one of `message` and `message_re` would be
> allowed at a time. (The PR for #35514 includes one test case that
> ''does'' use a in regex this way.)
>
> We should maybe also `re.escape()` the `module` arg (but without the
> `r".*"`) and add a `module_re` arg.
>
> == Compatibility ==
>
> `ignore_warnings()` seems to be documented only as an internal API for
> [https://docs.djangoproject.com/en/6.0/internals/contributing/writing-
> code/submitting-
> patches/#deprecating-a-feature:~:text=ignore_warnings%20decorator
> deprecating a feature]. But it's exposed in `django.test`. I'm not clear
> how to apply the deprecation policy to this.
>
> As of right now, none of Django's own tests in the main branch are using
> the `message` arg. (That will change when the PR for #35514 is merged.) A
> handful use `module`, none expecting regex behavior.
>
> If we ''do'' want to retain compatibility with a deprecation period, we
> could add a temporary `message_is_str` arg defaulting to `False`, and
> only escape the `message` arg if it's set True. If `False`, issue a
> deprecation warning and continue treating `message` as a regex during
> deprecation. After the deprecation period, switch to the new behavior and
> start a ''second'' deprecation period to remove the `message_is_str` arg.
>
> == Reproduction ==
>
> {{{#!python
> import re
> from django.test import SimpleTestCase, ignore_warnings
>
> class MessageArgsConsistencyTests(SimpleTestCase):
>     def setUp(self):
>         # Convert UserWarning to error within these tests.
>         self.enterClassContext(
>             warnings.catch_warnings(action="error", category=UserWarning)
>         )
>
>     def test_assert_warns_message(self):
>         # assertWarnsMessage() uses `expected in actual` substring check.
>         with self.assertWarnsMessage(UserWarning, "foo() will change"):
>             warnings.warn("Soon, foo() will change.",
> category=UserWarning)
>
>     def test_assert_warns_regex(self):
>         # assertWarnsRegex() uses `re.search(expected, actual)`.
>         with self.assertWarnsRegex(UserWarning, re.escape("foo() will
> change")):
>             warnings.warn("Soon, foo() will change.",
> category=UserWarning)
>
>     def test_does_ignore_warnings_work_like_assert_warns_message(self):
>         # No. This does not ignore the warning and fails with an error.
>         with ignore_warnings(category=UserWarning, message="foo() will
> change"):
>             warnings.warn("Soon, foo() will change.",
> category=UserWarning)
>
>     def test_does_ignore_warnings_work_like_assert_warns_regex(self):
>         # Is the problem that ignore_warnings() expects a regex?
>         # Not entirely. This also fails with an error.
>         with ignore_warnings(
>             category=UserWarning, message=re.escape("foo() will change")
>         ):
>             warnings.warn("Soon, foo() will change.",
> category=UserWarning)
>
>     def test_so_how_does_ignore_warnings_work(self):
>         # ignore_warnings() actually uses `re.match(expected, actual)`.
>         # This ignores the warning correctly.
>         with ignore_warnings(
>             category=UserWarning, message=r".*" + re.escape("foo() will
> change")
>         ):
>             warnings.warn("Soon, foo() will change.",
> category=UserWarning)
> }}}

New description:

 This use of `ignore_warnings()` does ''not'' ignore the warning:

 {{{#!python
 with ignore_warnings(
     category=RemovedInDjango70Warning,
     message="get_connection() is deprecated.",
 ):
     warnings.warn(
         "get_connection() is deprecated.",
         RemovedInDjango70Warning,
     )
 }}}

 That's inconsistent with how `assertWarnsMessage()` works, and is likely
 to be unexpected.

 Internally, `ignore_warnings()` uses Python's `warnings.catch_warnings()`,
 which passes the `message` arg directly to `re.match()`. (So
 `ignore_warnings()` is also inconsistent with `assertWarnsRegex()`, which
 uses `re.search()`.) See the reproduction example below.

 The `message` arg is not documented, but has some specific support in
 `ignore_warnings()`. (Same thing for the `module` arg, though that's less
 of an issue since the only regex pattern char commonly in module names is
 ".", and the pattern `r"."` matches the string `"."`.)

 == Proposal ==

 We should change `ignore_warnings()` to `re.escape()` the `message`  arg
 and prepend `r".*"` to it, making it consistent with
 `assertWarnsMessage()` by default.

 For test cases that do want ignore messages by regex, we would introduce a
 `message_re` arg. Only one of `message` and `message_re` would be allowed
 at a time. (The PR for #35514 includes one test case that ''does'' use a
 regex in this way.)

 We should maybe also `re.escape()` the `module` arg (but without the
 `r".*"`) and add a `module_re` arg.

 == Compatibility ==

 `ignore_warnings()` seems to be documented only as an internal API for
 [https://docs.djangoproject.com/en/6.0/internals/contributing/writing-code
 /submitting-
 patches/#deprecating-a-feature:~:text=ignore_warnings%20decorator
 deprecating a feature]. But it's exposed in `django.test`. I'm not clear
 how to apply the deprecation policy to this.

 As of right now, none of Django's own tests in the main branch are using
 the `message` arg. (That will change when the PR for #35514 is merged.) A
 handful use `module`, none expecting regex behavior.

 If we ''do'' want to retain compatibility with a deprecation period, we
 could add a temporary `message_is_str` arg defaulting to `False`, and only
 escape the `message` arg if it's set True. If `False`, issue a deprecation
 warning and continue treating `message` as a regex during deprecation.
 After the deprecation period, switch to the new behavior and start a
 ''second'' deprecation period to remove the `message_is_str` arg.

 == Reproduction ==

 {{{#!python
 import re
 from django.test import SimpleTestCase, ignore_warnings

 class MessageArgsConsistencyTests(SimpleTestCase):
     def setUp(self):
         # Convert UserWarning to error within these tests.
         self.enterClassContext(
             warnings.catch_warnings(action="error", category=UserWarning)
         )

     def test_assert_warns_message(self):
         # assertWarnsMessage() uses `expected in actual` substring check.
         with self.assertWarnsMessage(UserWarning, "foo() will change"):
             warnings.warn("Soon, foo() will change.",
 category=UserWarning)

     def test_assert_warns_regex(self):
         # assertWarnsRegex() uses `re.search(expected, actual)`.
         with self.assertWarnsRegex(UserWarning, re.escape("foo() will
 change")):
             warnings.warn("Soon, foo() will change.",
 category=UserWarning)

     def test_does_ignore_warnings_work_like_assert_warns_message(self):
         # No. This does not ignore the warning and fails with an error.
         with ignore_warnings(category=UserWarning, message="foo() will
 change"):
             warnings.warn("Soon, foo() will change.",
 category=UserWarning)

     def test_does_ignore_warnings_work_like_assert_warns_regex(self):
         # Is the problem that ignore_warnings() expects a regex?
         # Not entirely. This also fails with an error.
         with ignore_warnings(
             category=UserWarning, message=re.escape("foo() will change")
         ):
             warnings.warn("Soon, foo() will change.",
 category=UserWarning)

     def test_so_how_does_ignore_warnings_work(self):
         # ignore_warnings() actually uses `re.match(expected, actual)`.
         # This ignores the warning correctly.
         with ignore_warnings(
             category=UserWarning, message=r".*" + re.escape("foo() will
 change")
         ):
             warnings.warn("Soon, foo() will change.",
 category=UserWarning)
 }}}

--
Comment (by Mike Edmunds):

 (I'm not sure if this is a bug or feature request, and whether the
 component should be "Testing framework" or internal "Utilities".)
-- 
Ticket URL: <https://code.djangoproject.com/ticket/37089#comment:1>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/django-updates/0107019e0464461e-f68957ce-05e1-4f94-a29d-dcdaaba91510-000000%40eu-central-1.amazonses.com.

Reply via email to