Skip to content

Commit b109915

Browse files
authored
Merge pull request #3382 from bdarnell/remove-testmethodwrapper
testing: Replace _TestMethodWrapper with _callTestMethod
2 parents f399f40 + 180332a commit b109915

File tree

2 files changed

+33
-53
lines changed

2 files changed

+33
-53
lines changed

tornado/test/testing_test.py

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from tornado.web import Application
66
import asyncio
77
import contextlib
8-
import inspect
98
import gc
109
import os
1110
import platform
@@ -118,7 +117,11 @@ def tearDown(self):
118117
super().tearDown()
119118

120119

121-
class AsyncTestCaseWrapperTest(unittest.TestCase):
120+
class AsyncTestCaseReturnAssertionsTest(unittest.TestCase):
121+
# These tests verify that tests that return non-None values (without being decorated with
122+
# @gen_test) raise errors instead of incorrectly succeeding. These tests should be removed or
123+
# updated when the _callTestMethod method is removed from AsyncTestCase (the same checks will
124+
# still happen, but they'll be performed in the stdlib as DeprecationWarnings)
122125
def test_undecorated_generator(self):
123126
class Test(AsyncTestCase):
124127
def test_gen(self):
@@ -135,7 +138,10 @@ def test_gen(self):
135138
"pypy destructor warnings cannot be silenced",
136139
)
137140
@unittest.skipIf(
138-
sys.version_info >= (3, 12), "py312 has its own check for test case returns"
141+
# This check actually exists in 3.11 but it changed in 3.12 in a way that breaks
142+
# this test.
143+
sys.version_info >= (3, 12),
144+
"py312 has its own check for test case returns",
139145
)
140146
def test_undecorated_coroutine(self):
141147
class Test(AsyncTestCase):
@@ -176,17 +182,6 @@ def test_other_return(self):
176182
self.assertEqual(len(result.errors), 1)
177183
self.assertIn("Return value from test method ignored", result.errors[0][1])
178184

179-
def test_unwrap(self):
180-
class Test(AsyncTestCase):
181-
def test_foo(self):
182-
pass
183-
184-
test = Test("test_foo")
185-
self.assertIs(
186-
inspect.unwrap(test.test_foo),
187-
test.test_foo.orig_method, # type: ignore[attr-defined]
188-
)
189-
190185

191186
class SetUpTearDownTest(unittest.TestCase):
192187
def test_set_up_tear_down(self):

tornado/testing.py

Lines changed: 24 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -84,39 +84,6 @@ def get_async_test_timeout() -> float:
8484
return 5
8585

8686

87-
class _TestMethodWrapper(object):
88-
"""Wraps a test method to raise an error if it returns a value.
89-
90-
This is mainly used to detect undecorated generators (if a test
91-
method yields it must use a decorator to consume the generator),
92-
but will also detect other kinds of return values (these are not
93-
necessarily errors, but we alert anyway since there is no good
94-
reason to return a value from a test).
95-
"""
96-
97-
def __init__(self, orig_method: Callable) -> None:
98-
self.orig_method = orig_method
99-
self.__wrapped__ = orig_method
100-
101-
def __call__(self, *args: Any, **kwargs: Any) -> None:
102-
result = self.orig_method(*args, **kwargs)
103-
if isinstance(result, Generator) or inspect.iscoroutine(result):
104-
raise TypeError(
105-
"Generator and coroutine test methods should be"
106-
" decorated with tornado.testing.gen_test"
107-
)
108-
elif result is not None:
109-
raise ValueError("Return value from test method ignored: %r" % result)
110-
111-
def __getattr__(self, name: str) -> Any:
112-
"""Proxy all unknown attributes to the original method.
113-
114-
This is important for some of the decorators in the `unittest`
115-
module, such as `unittest.skipIf`.
116-
"""
117-
return getattr(self.orig_method, name)
118-
119-
12087
class AsyncTestCase(unittest.TestCase):
12188
"""`~unittest.TestCase` subclass for testing `.IOLoop`-based
12289
asynchronous code.
@@ -173,12 +140,6 @@ def __init__(self, methodName: str = "runTest") -> None:
173140
self.__stop_args = None # type: Any
174141
self.__timeout = None # type: Optional[object]
175142

176-
# It's easy to forget the @gen_test decorator, but if you do
177-
# the test will silently be ignored because nothing will consume
178-
# the generator. Replace the test method with a wrapper that will
179-
# make sure it's not an undecorated generator.
180-
setattr(self, methodName, _TestMethodWrapper(getattr(self, methodName)))
181-
182143
# Not used in this class itself, but used by @gen_test
183144
self._test_generator = None # type: Optional[Union[Generator, Coroutine]]
184145

@@ -289,6 +250,30 @@ def run(
289250
self.__rethrow()
290251
return ret
291252

253+
def _callTestMethod(self, method: Callable) -> None:
254+
"""Run the given test method, raising an error if it returns non-None.
255+
256+
Failure to decorate asynchronous test methods with ``@gen_test`` can lead to tests
257+
incorrectly passing.
258+
259+
Remove this override when Python 3.10 support is dropped. This check (in the form of a
260+
DeprecationWarning) became a part of the standard library in 3.11.
261+
262+
Note that ``_callTestMethod`` is not documented as a public interface. However, it is
263+
present in all supported versions of Python (3.8+), and if it goes away in the future that's
264+
OK because we can just remove this override as noted above.
265+
"""
266+
# Calling super()._callTestMethod would hide the return value, even in python 3.8-3.10
267+
# where the check isn't being done for us.
268+
result = method()
269+
if isinstance(result, Generator) or inspect.iscoroutine(result):
270+
raise TypeError(
271+
"Generator and coroutine test methods should be"
272+
" decorated with tornado.testing.gen_test"
273+
)
274+
elif result is not None:
275+
raise ValueError("Return value from test method ignored: %r" % result)
276+
292277
def stop(self, _arg: Any = None, **kwargs: Any) -> None:
293278
"""Stops the `.IOLoop`, causing one pending (or future) call to `wait()`
294279
to return.

0 commit comments

Comments
 (0)