diff --git a/changelog/3691.bugfix.rst b/changelog/3691.bugfix.rst new file mode 100644 index 000000000..f30dd67a1 --- /dev/null +++ b/changelog/3691.bugfix.rst @@ -0,0 +1,2 @@ +Python 2: safely format warning message about passing unicode strings to ``warnings.warn``, which may cause +surprising ``MemoryError`` exception when monkey patching ``warnings.warn`` itself. diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 5574eee8e..e900ff067 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -123,7 +123,7 @@ def warning_record_to_str(warning_message): if unicode_warning: warnings.warn( "Warning is using unicode non convertible to ascii, " - "converting to a safe representation:\n %s" % msg, + "converting to a safe representation:\n {!r}".format(compat.safe_str(msg)), UnicodeWarning, ) return msg diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 7825f2167..10eb5ea33 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -3,6 +3,8 @@ from __future__ import unicode_literals import sys +import six + import pytest @@ -562,3 +564,30 @@ class TestDeprecationWarningsByDefault: monkeypatch.setenv(str("PYTHONWARNINGS"), str("once::UserWarning")) result = testdir.runpytest_subprocess() assert WARNINGS_SUMMARY_HEADER not in result.stdout.str() + + +@pytest.mark.skipif(six.PY3, reason="Python 2 only issue") +def test_infinite_loop_warning_against_unicode_usage_py2(testdir): + """ + We need to be careful when raising the warning about unicode usage with "warnings.warn" + because it might be overwritten by users and this itself causes another warning (#3691). + """ + testdir.makepyfile( + """ + # -*- coding: utf8 -*- + from __future__ import unicode_literals + import warnings + import pytest + + def _custom_showwarning(message, *a, **b): + return "WARNING: {}".format(message) + + warnings.formatwarning = _custom_showwarning + + @pytest.mark.filterwarnings("default") + def test_custom_warning_formatter(): + warnings.warn("¥") + """ + ) + result = testdir.runpytest_subprocess() + result.stdout.fnmatch_lines(["*1 passed, * warnings in*"])