From 123289a88ee01cfc3083fee22e0161cf4c9e4087 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 19 Jan 2017 11:38:15 +0100 Subject: [PATCH 1/2] fixes #2208 by introducing a iteration limit --- CHANGELOG.rst | 5 +++++ _pytest/compat.py | 12 ++++++++++-- testing/test_compat.py | 33 +++++++++++++++++++++++++++++---- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 41d316dd561..3d78d3273fa 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -41,6 +41,10 @@ Changes * fix `#2013`_: turn RecordedWarning into namedtupe, to give it a comprehensible repr while preventing unwarranted modification +* fix `#2208`_: ensure a iteration limit for _pytest.compat.get_real_func. + Thanks `@RonnyPfannschmidt`_ for the Report and PR + + .. _@davidszotten: https://github.com/davidszotten .. _@fushi: https://github.com/fushi .. _@mattduck: https://github.com/mattduck @@ -57,6 +61,7 @@ Changes .. _#2101: https://github.com/pytest-dev/pytest/pull/2101 .. _#2166: https://github.com/pytest-dev/pytest/pull/2166 .. _#2147: https://github.com/pytest-dev/pytest/issues/2147 +.. _#2208: https://github.com/pytest-dev/pytest/issues/2208 3.0.6.dev0 (unreleased) ======================= diff --git a/_pytest/compat.py b/_pytest/compat.py index 2f5e75cf1a6..09f681c8658 100644 --- a/_pytest/compat.py +++ b/_pytest/compat.py @@ -180,8 +180,16 @@ def get_real_func(obj): """ gets the real function object of the (possibly) wrapped object by functools.wraps or functools.partial. """ - while hasattr(obj, "__wrapped__"): - obj = obj.__wrapped__ + start_obj = obj + for i in range(100): + new_obj = getattr(obj, '__wrapped__', None) + if new_obj is None: + break + obj = new_obj + else: + raise ValueError( + ("could not find real function of {start}" + "\nstopped at {current}").format(start=start_obj, current=obj)) if isinstance(obj, functools.partial): obj = obj.func return obj diff --git a/testing/test_compat.py b/testing/test_compat.py index 1fdd07e29c6..1736e8e2a8b 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -1,7 +1,7 @@ import sys import pytest -from _pytest.compat import is_generator +from _pytest.compat import is_generator, get_real_func def test_is_generator(): @@ -15,7 +15,30 @@ def foo(): assert not is_generator(foo) -@pytest.mark.skipif(sys.version_info < (3, 4), reason='asyncio available in Python 3.4+') +def test_real_func_loop_limit(): + + class Evil(object): + def __init__(self): + self.left = 1000 + + def __repr__(self): + return "".format(left=self.left) + + def __getattr__(self, attr): + if not self.left: + raise RuntimeError('its over') + self.left -= 1 + return self + + evil = Evil() + + with pytest.raises(ValueError): + res = get_real_func(evil) + print(res) + + +@pytest.mark.skipif(sys.version_info < (3, 4), + reason='asyncio available in Python 3.4+') def test_is_generator_asyncio(testdir): testdir.makepyfile(""" from _pytest.compat import is_generator @@ -27,12 +50,14 @@ def baz(): def test_is_generator_asyncio(): assert not is_generator(baz) """) - # avoid importing asyncio into pytest's own process, which in turn imports logging (#8) + # avoid importing asyncio into pytest's own process, + # which in turn imports logging (#8) result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines(['*1 passed*']) -@pytest.mark.skipif(sys.version_info < (3, 5), reason='async syntax available in Python 3.5+') +@pytest.mark.skipif(sys.version_info < (3, 5), + reason='async syntax available in Python 3.5+') def test_is_generator_async_syntax(testdir): testdir.makepyfile(""" from _pytest.compat import is_generator From 250597d468149121fd6d8ae8f867fbe8faf843a2 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 19 Jan 2017 13:05:58 +0100 Subject: [PATCH 2/2] get_real_func: use saferepr when formatting the error message --- _pytest/compat.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/_pytest/compat.py b/_pytest/compat.py index 09f681c8658..e097dee51a1 100644 --- a/_pytest/compat.py +++ b/_pytest/compat.py @@ -189,7 +189,9 @@ def get_real_func(obj): else: raise ValueError( ("could not find real function of {start}" - "\nstopped at {current}").format(start=start_obj, current=obj)) + "\nstopped at {current}").format( + start=py.io.saferepr(start_obj), + current=py.io.saferepr(obj))) if isinstance(obj, functools.partial): obj = obj.func return obj