Skip to content

Commit

Permalink
Expand warnings output for ResourceWarning
Browse files Browse the repository at this point in the history
  • Loading branch information
nicoddemus committed Feb 13, 2022
1 parent c01a5c1 commit cdd827b
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 0 deletions.
4 changes: 4 additions & 0 deletions changelog/9644.improvement.rst
@@ -0,0 +1,4 @@
More information about the location of resources that led Python to raise :class:`ResourceWarning` can now
be obtained by enabling :mod:`tracemalloc`.

See :ref:`resource-warnings` for more information.
15 changes: 15 additions & 0 deletions doc/en/how-to/capture-warnings.rst
Expand Up @@ -441,3 +441,18 @@ Please read our :ref:`backwards-compatibility` to learn how we proceed about dep
features.

The full list of warnings is listed in :ref:`the reference documentation <warnings ref>`.


.. _`resource-warnings`:

Resource Warnings
-----------------

Additional information of the source of a :class:`ResourceWarning` can be obtained when captured by pytest if
:mod:`tracemalloc` module is enabled.

One convenient way to enable :mod:`tracemalloc` when running tests is to set the :envvar:`PYTHONTRACEMALLOC` to a large
enough number of frames (say ``20``, but that number is application dependent).

For more information, consult the `Python Development Mode <https://docs.python.org/3/library/devmode.html>`__
section in the Python documentation.
19 changes: 19 additions & 0 deletions src/_pytest/warnings.py
Expand Up @@ -81,6 +81,25 @@ def warning_record_to_str(warning_message: warnings.WarningMessage) -> str:
warning_message.lineno,
warning_message.line,
)
if warning_message.source is not None:
try:
import tracemalloc
except ImportError:
pass
else:
tb = tracemalloc.get_object_traceback(warning_message.source)
if tb is not None:
formatted_tb = "\n".join(tb.format())
# Use a leading new line to better separate the (large) output
# from the traceback to the previous warning text.
msg += (
f"\nObject allocated at (most recent call first):\n{formatted_tb}"
)
else:
# No need for a leading new line.
url = "https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings"
msg += "Enable tracemalloc to get traceback where the object was allocated.\n"
msg += f"See {url} for more info."
return msg


Expand Down
51 changes: 51 additions & 0 deletions testing/test_warnings.py
@@ -1,4 +1,5 @@
import os
import sys
import warnings
from typing import List
from typing import Optional
Expand Down Expand Up @@ -774,3 +775,53 @@ def test_it():
"*Unknown pytest.mark.unknown*",
]
)


def test_resource_warning(pytester: Pytester, monkeypatch: pytest.MonkeyPatch) -> None:
# Some platforms (notably PyPy) don't have tracemalloc.
# We choose to explicitly not skip this in case tracemalloc is not
# available, using `importorskip("tracemalloc")` for example,
# because we want to ensure the same code path does not break in those platforms.
try:
import tracemalloc # noqa

has_tracemalloc = True
except ImportError:
has_tracemalloc = False

pytester.makepyfile(
"""
def open_file(p):
f = p.open("r")
assert p.read_text() == "hello"
def test_resource_warning(tmp_path):
p = tmp_path.joinpath("foo.txt")
p.write_text("hello")
open_file(p)
"""
)
result = pytester.run(sys.executable, "-Xdev", "-m", "pytest")
expected_extra = (
[
"*ResourceWarning* unclosed file*",
"*Enable tracemalloc to get traceback where the object was allocated*",
"*See https* for more info.",
]
if has_tracemalloc
else []
)
result.stdout.fnmatch_lines([*expected_extra, "*1 passed*"])

monkeypatch.setenv("PYTHONTRACEMALLOC", "20")

result = pytester.run(sys.executable, "-Xdev", "-m", "pytest")
expected_extra = (
[
"*ResourceWarning* unclosed file*",
"*Object allocated at (most recent call first)*",
]
if has_tracemalloc
else []
)
result.stdout.fnmatch_lines([*expected_extra, "*1 passed*"])

0 comments on commit cdd827b

Please sign in to comment.