Skip to content

Commit fc304b8

Browse files
authored
Merge pull request #2006 from MSeifert04/fix-1965
Fix memory leak with pytest.raises by using weakref
2 parents 0b94c43 + 1130b9f commit fc304b8

File tree

5 files changed

+50
-6
lines changed

5 files changed

+50
-6
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ mbyt
9999
Michael Aquilina
100100
Michael Birtwell
101101
Michael Droettboom
102+
Michael Seifert
102103
Mike Lundy
103104
Nicolas Delaby
104105
Oleg Pidsadnyi

CHANGELOG.rst

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
* When loading plugins, import errors which contain non-ascii messages are now properly handled in Python 2 (`#1998`_).
1313
Thanks `@nicoddemus`_ for the PR.
1414

15+
* Fixed cyclic reference when ``pytest.raises`` is used in context-manager form (`#1965`_). Also as a
16+
result of this fix, ``sys.exc_info()`` is left empty in both context-manager and function call usages.
17+
Previously, ``sys.exc_info`` would contain the exception caught by the context manager,
18+
even when the expected exception occurred.
19+
Thanks `@MSeifert04`_ for the report and the PR.
20+
1521
* Fixed false-positives warnings from assertion rewrite hook for modules that were rewritten but
1622
were later marked explicitly by ``pytest.register_assert_rewrite``
1723
or implicitly as a plugin (`#2005`_).
@@ -36,12 +42,14 @@
3642

3743
.. _@adborden: https://github.com/adborden
3844
.. _@cwitty: https://github.com/cwitty
39-
.. _@okulynyak: https://github.com/okulynyak
40-
.. _@matclab: https://github.com/matclab
41-
.. _@gdyuldin: https://github.com/gdyuldin
4245
.. _@d_b_w: https://github.com/d_b_w
46+
.. _@gdyuldin: https://github.com/gdyuldin
47+
.. _@matclab: https://github.com/matclab
48+
.. _@MSeifert04: https://github.com/MSeifert04
49+
.. _@okulynyak: https://github.com/okulynyak
4350

4451
.. _#442: https://github.com/pytest-dev/pytest/issues/442
52+
.. _#1965: https://github.com/pytest-dev/pytest/issues/1965
4553
.. _#1976: https://github.com/pytest-dev/pytest/issues/1976
4654
.. _#1984: https://github.com/pytest-dev/pytest/issues/1984
4755
.. _#1998: https://github.com/pytest-dev/pytest/issues/1998

_pytest/_code/code.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import sys
22
from inspect import CO_VARARGS, CO_VARKEYWORDS
33
import re
4+
from weakref import ref
45

56
import py
67
builtin_repr = repr
@@ -230,7 +231,7 @@ def ishidden(self):
230231
return False
231232

232233
if py.builtin.callable(tbh):
233-
return tbh(self._excinfo)
234+
return tbh(None if self._excinfo is None else self._excinfo())
234235
else:
235236
return tbh
236237

@@ -370,7 +371,7 @@ def __init__(self, tup=None, exprinfo=None):
370371
#: the exception type name
371372
self.typename = self.type.__name__
372373
#: the exception traceback (_pytest._code.Traceback instance)
373-
self.traceback = _pytest._code.Traceback(self.tb, excinfo=self)
374+
self.traceback = _pytest._code.Traceback(self.tb, excinfo=ref(self))
374375

375376
def __repr__(self):
376377
return "<ExceptionInfo %s tblen=%d>" % (self.typename, len(self.traceback))

_pytest/python.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1237,7 +1237,11 @@ def __exit__(self, *tp):
12371237
exc_type, value, traceback = tp
12381238
tp = exc_type, exc_type(value), traceback
12391239
self.excinfo.__init__(tp)
1240-
return issubclass(self.excinfo.type, self.expected_exception)
1240+
suppress_exception = issubclass(self.excinfo.type, self.expected_exception)
1241+
if sys.version_info[0] == 2 and suppress_exception:
1242+
sys.exc_clear()
1243+
return suppress_exception
1244+
12411245

12421246
# builtin pytest.approx helper
12431247

testing/python/raises.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import pytest
2+
import sys
3+
24

35
class TestRaises:
46
def test_raises(self):
@@ -96,3 +98,31 @@ def test_custom_raise_message(self):
9698
assert e.msg == message
9799
else:
98100
assert False, "Expected pytest.raises.Exception"
101+
102+
@pytest.mark.parametrize('method', ['function', 'with'])
103+
def test_raises_cyclic_reference(self, method):
104+
"""
105+
Ensure pytest.raises does not leave a reference cycle (#1965).
106+
"""
107+
import gc
108+
109+
class T(object):
110+
def __call__(self):
111+
raise ValueError
112+
113+
t = T()
114+
if method == 'function':
115+
pytest.raises(ValueError, t)
116+
else:
117+
with pytest.raises(ValueError):
118+
t()
119+
120+
# ensure both forms of pytest.raises don't leave exceptions in sys.exc_info()
121+
assert sys.exc_info() == (None, None, None)
122+
123+
del t
124+
125+
# ensure the t instance is not stuck in a cyclic reference
126+
for o in gc.get_objects():
127+
assert type(o) is not T
128+

0 commit comments

Comments
 (0)