diff --git a/Lib/test/test_except_star.py b/Lib/test/test_except_star.py index 3e0f8caa9b2..807e7c5a5d6 100644 --- a/Lib/test/test_except_star.py +++ b/Lib/test/test_except_star.py @@ -84,7 +84,8 @@ def test_break_in_except_star(self): if i == 2: break finally: - return 0 + pass + return 0 """) @@ -117,7 +118,8 @@ def test_continue_in_except_star_block_invalid(self): if i == 2: continue finally: - return 0 + pass + return 0 """) def test_return_in_except_star_block_invalid(self): @@ -889,8 +891,7 @@ def test_raise_handle_all_raise_two_unnamed(self): class TestExceptStarExceptionGroupSubclass(ExceptStarTest): - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON def test_except_star_EG_subclass(self): class EG(ExceptionGroup): def __new__(cls, message, excs, code): @@ -1217,4 +1218,4 @@ def test_reraise_unhashable_eg(self): if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/Lib/test/test_exception_hierarchy.py b/Lib/test/test_exception_hierarchy.py index e8c1c7fd1e7..3472019ea13 100644 --- a/Lib/test/test_exception_hierarchy.py +++ b/Lib/test/test_exception_hierarchy.py @@ -146,8 +146,7 @@ def test_errno_translation(self): self.assertEqual(e.strerror, "File already exists") self.assertEqual(e.filename, "foo.txt") - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON def test_blockingioerror(self): args = ("a", "b", "c", "d", "e") for n in range(6): @@ -182,8 +181,7 @@ def test_init_kwdargs(self): self.assertEqual(e.bar, "baz") self.assertEqual(e.args, ("some message",)) - # TODO: RUSTPYTHON - @unittest.expectedFailure + @unittest.expectedFailure # TODO: RUSTPYTHON def test_new_overridden(self): e = SubOSErrorWithNew("some message", "baz") self.assertEqual(e.baz, "baz") diff --git a/Lib/test/test_exception_variations.py b/Lib/test/test_exception_variations.py index e103eaf8466..a83a41d2975 100644 --- a/Lib/test/test_exception_variations.py +++ b/Lib/test/test_exception_variations.py @@ -294,8 +294,6 @@ def test_nested_exception_in_finally_with_exception(self): self.assertTrue(hit_except) -# TODO: RUSTPYTHON -''' class ExceptStarTestCases(unittest.TestCase): def test_try_except_else_finally(self): hit_except = False @@ -571,7 +569,7 @@ def test_nested_else_mixed2(self): self.assertFalse(hit_else) self.assertTrue(hit_finally) self.assertTrue(hit_except) -''' + if __name__ == '__main__': unittest.main() diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 0c273935cb4..04af299dea3 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -60,7 +60,7 @@ def raise_catch(self, exc, excname): self.assertEqual(buf1, buf2) self.assertEqual(exc.__name__, excname) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def testRaising(self): self.raise_catch(AttributeError, "AttributeError") self.assertRaises(AttributeError, getattr, sys, "undefined_attribute") @@ -145,7 +145,7 @@ def testRaising(self): self.raise_catch(StopAsyncIteration, "StopAsyncIteration") - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def testSyntaxErrorMessage(self): # make sure the right exception message is raised for each of # these code fragments @@ -170,7 +170,7 @@ def ckmsg(src, msg): ckmsg("continue\n", "'continue' not properly in loop") ckmsg("f'{6 0}'", "invalid syntax. Perhaps you forgot a comma?") - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def testSyntaxErrorMissingParens(self): def ckmsg(src, msg, exception=SyntaxError): try: @@ -227,14 +227,16 @@ def check(self, src, lineno, offset, end_lineno=None, end_offset=None, encoding= if not isinstance(src, str): src = src.decode(encoding, 'replace') line = src.split('\n')[lineno-1] + if lineno == 1: + line = line.removeprefix('\ufeff') self.assertIn(line, cm.exception.text) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_error_offset_continuation_characters(self): check = self.check check('"\\\n"(1 for c in I,\\\n\\', 2, 2) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def testSyntaxErrorOffset(self): check = self.check check('def fact(x):\n\treturn x!\n', 2, 10) @@ -244,7 +246,9 @@ def testSyntaxErrorOffset(self): check('Python = "\u1e54\xfd\u0163\u0125\xf2\xf1" +', 1, 20) check(b'# -*- coding: cp1251 -*-\nPython = "\xcf\xb3\xf2\xee\xed" +', 2, 19, encoding='cp1251') - check(b'Python = "\xcf\xb3\xf2\xee\xed" +', 1, 10) + check(b'Python = "\xcf\xb3\xf2\xee\xed" +', 1, 12) + check(b'\n\n\nPython = "\xcf\xb3\xf2\xee\xed" +', 4, 12) + check(b'\xef\xbb\xbfPython = "\xcf\xb3\xf2\xee\xed" +', 1, 12) check('x = "a', 1, 5) check('lambda x: x = 2', 1, 1) check('f{a + b + c}', 1, 2) @@ -292,7 +296,7 @@ def baz(): check("pass\npass\npass\n(1+)\npass\npass\npass", 4, 4) check("(1+)", 1, 4) check("[interesting\nfoo()\n", 1, 1) - check(b"\xef\xbb\xbf#coding: utf8\nprint('\xe6\x88\x91')\n", 0, -1) + check(b"\xef\xbb\xbf#coding: utf8\nprint('\xe6\x88\x91')\n", 1, 0) check("""f''' { (123_a) @@ -362,7 +366,7 @@ def test_capi1(): except TypeError as err: co = err.__traceback__.tb_frame.f_code self.assertEqual(co.co_name, "test_capi1") - self.assertTrue(co.co_filename.endswith('test_exceptions.py')) + self.assertEndsWith(co.co_filename, 'test_exceptions.py') else: self.fail("Expected exception") @@ -374,7 +378,7 @@ def test_capi2(): tb = err.__traceback__.tb_next co = tb.tb_frame.f_code self.assertEqual(co.co_name, "__init__") - self.assertTrue(co.co_filename.endswith('test_exceptions.py')) + self.assertEndsWith(co.co_filename, 'test_exceptions.py') co2 = tb.tb_frame.f_back.f_code self.assertEqual(co2.co_name, "test_capi2") else: @@ -428,9 +432,9 @@ def test_WindowsError(self): self.assertEqual(w.filename, None) self.assertEqual(w.filename2, None) + @unittest.expectedFailure # TODO: RUSTPYTHON @unittest.skipUnless(sys.platform == 'win32', 'test specific to Windows') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_windows_message(self): """Should fill in unknown error code in Windows error message""" ctypes = import_module('ctypes') @@ -439,7 +443,7 @@ def test_windows_message(self): with self.assertRaisesRegex(OSError, 'Windows Error 0x%x' % code): ctypes.pythonapi.PyErr_SetFromWindowsErr(code) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def testAttributes(self): # test that exception attributes are happy @@ -605,7 +609,7 @@ def test_invalid_setstate(self): def test_notes(self): for e in [BaseException(1), Exception(2), ValueError(3)]: with self.subTest(e=e): - self.assertFalse(hasattr(e, '__notes__')) + self.assertNotHasAttr(e, '__notes__') e.add_note("My Note") self.assertEqual(e.__notes__, ["My Note"]) @@ -617,7 +621,7 @@ def test_notes(self): self.assertEqual(e.__notes__, ["My Note", "Your Note"]) del e.__notes__ - self.assertFalse(hasattr(e, '__notes__')) + self.assertNotHasAttr(e, '__notes__') e.add_note("Our Note") self.assertEqual(e.__notes__, ["Our Note"]) @@ -658,7 +662,7 @@ def testInvalidTraceback(self): else: self.fail("No exception raised") - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_invalid_setattr(self): TE = TypeError exc = Exception() @@ -671,7 +675,7 @@ def test_invalid_setattr(self): msg = "exception context must be None or derive from BaseException" self.assertRaisesRegex(TE, msg, setattr, exc, '__context__', 1) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_invalid_delattr(self): TE = TypeError try: @@ -743,7 +747,7 @@ def __init__(self, fancy_arg): x = DerivedException(fancy_arg=42) self.assertEqual(x.fancy_arg, 42) - @unittest.skipIf(sys.platform == 'win32', 'TODO: RUSTPYTHON; Windows') + @unittest.skipIf(sys.platform == "win32", "TODO: RUSTPYTHON; Windows") @no_tracing def testInfiniteRecursion(self): def f(): @@ -1304,7 +1308,7 @@ def test_context_of_exception_in_else_and_finally(self): self.assertIs(exc, oe) self.assertIs(exc.__context__, ve) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_unicode_change_attributes(self): # See issue 7309. This was a crasher. @@ -1348,7 +1352,7 @@ def test_unicode_errors_no_object(self): for klass in klasses: self.assertEqual(str(klass.__new__(klass)), "") - @unittest.expectedFailure # TODO: RUSTPYTHON; OverflowError: Python int too large to convert to Rust usize + @unittest.expectedFailure # TODO: RUSTPYTHON; OverflowError: Python int too large to convert to Rust usize def test_unicode_error_str_does_not_crash(self): # Test that str(UnicodeError(...)) does not crash. # See https://github.com/python/cpython/issues/123378. @@ -1372,7 +1376,46 @@ def test_unicode_error_str_does_not_crash(self): exc = UnicodeDecodeError('utf-8', encoded, start, end, '') self.assertIsInstance(str(exc), str) - @unittest.skipIf(sys.platform == 'win32', 'TODO: RUSTPYTHON; Windows') + @unittest.expectedFailure # TODO: RUSTPYTHON + def test_unicode_error_evil_str_set_none_object(self): + def side_effect(exc): + exc.object = None + self.do_test_unicode_error_mutate(side_effect) + + @unittest.expectedFailure # TODO: RUSTPYTHON + def test_unicode_error_evil_str_del_self_object(self): + def side_effect(exc): + del exc.object + self.do_test_unicode_error_mutate(side_effect) + + def do_test_unicode_error_mutate(self, side_effect): + # Test that str(UnicodeError(...)) does not crash when + # side-effects mutate the underlying 'object' attribute. + # See https://github.com/python/cpython/issues/128974. + + class Evil(str): + def __str__(self): + side_effect(exc) + return self + + for reason, encoding in [ + ("reason", Evil("utf-8")), + (Evil("reason"), "utf-8"), + (Evil("reason"), Evil("utf-8")), + ]: + with self.subTest(encoding=encoding, reason=reason): + with self.subTest(UnicodeEncodeError): + exc = UnicodeEncodeError(encoding, "x", 0, 1, reason) + self.assertRaises(TypeError, str, exc) + with self.subTest(UnicodeDecodeError): + exc = UnicodeDecodeError(encoding, b"x", 0, 1, reason) + self.assertRaises(TypeError, str, exc) + + with self.subTest(UnicodeTranslateError): + exc = UnicodeTranslateError("x", 0, 1, Evil("reason")) + self.assertRaises(TypeError, str, exc) + + @unittest.skipIf(sys.platform == "win32", "TODO: RUSTPYTHON; Windows") @no_tracing def test_badisinstance(self): # Bug #2542: if issubclass(e, MyException) raises an exception, @@ -1404,7 +1447,8 @@ def g(): self.assertIsInstance(exc, RecursionError, type(exc)) self.assertIn("maximum recursion depth exceeded", str(exc)) - + @support.skip_wasi_stack_overflow() + @support.skip_emscripten_stack_overflow() @cpython_only @support.requires_resource('cpu') def test_trashcan_recursion(self): @@ -1420,6 +1464,7 @@ def foo(): foo() support.gc_collect() + @support.skip_emscripten_stack_overflow() @cpython_only def test_recursion_normalizing_exception(self): import_module("_testinternalcapi") @@ -1492,11 +1537,12 @@ def test_recursion_normalizing_infinite_exception(self): """ rc, out, err = script_helper.assert_python_failure("-c", code) self.assertEqual(rc, 1) - expected = b'RecursionError: maximum recursion depth exceeded' + expected = b'RecursionError' self.assertTrue(expected in err, msg=f"{expected!r} not found in {err[:3_000]!r}... (truncated)") self.assertIn(b'Done.', out) + @support.skip_emscripten_stack_overflow() def test_recursion_in_except_handler(self): def set_relative_recursion_limit(n): @@ -1602,7 +1648,7 @@ def test_exception_with_doc(self): # test basic usage of PyErr_NewException error1 = _testcapi.make_exception_with_doc("_testcapi.error1") self.assertIs(type(error1), type) - self.assertTrue(issubclass(error1, Exception)) + self.assertIsSubclass(error1, Exception) self.assertIsNone(error1.__doc__) # test with given docstring @@ -1612,21 +1658,21 @@ def test_exception_with_doc(self): # test with explicit base (without docstring) error3 = _testcapi.make_exception_with_doc("_testcapi.error3", base=error2) - self.assertTrue(issubclass(error3, error2)) + self.assertIsSubclass(error3, error2) # test with explicit base tuple class C(object): pass error4 = _testcapi.make_exception_with_doc("_testcapi.error4", doc4, (error3, C)) - self.assertTrue(issubclass(error4, error3)) - self.assertTrue(issubclass(error4, C)) + self.assertIsSubclass(error4, error3) + self.assertIsSubclass(error4, C) self.assertEqual(error4.__doc__, doc4) # test with explicit dictionary error5 = _testcapi.make_exception_with_doc("_testcapi.error5", "", error4, {'a': 1}) - self.assertTrue(issubclass(error5, error4)) + self.assertIsSubclass(error5, error4) self.assertEqual(error5.a, 1) self.assertEqual(error5.__doc__, "") @@ -1654,7 +1700,7 @@ def inner(): gc_collect() # For PyPy or other GCs. self.assertEqual(wr(), None) - @unittest.skipIf(sys.platform == 'win32', 'TODO: RUSTPYTHON; Windows') + @unittest.skipIf(sys.platform == "win32", "TODO: RUSTPYTHON; Windows") @no_tracing def test_recursion_error_cleanup(self): # Same test as above, but with "recursion exceeded" errors @@ -1676,13 +1722,14 @@ def inner(): gc_collect() # For PyPy or other GCs. self.assertEqual(wr(), None) - @unittest.skipIf(sys.platform == 'win32', 'TODO: RUSTPYTHON; error specific to cpython') + @unittest.skipIf(sys.platform == "win32", "TODO: RUSTPYTHON; error specific to cpython") def test_errno_ENOTDIR(self): # Issue #12802: "not a directory" errors are ENOTDIR even on Windows with self.assertRaises(OSError) as cm: os.listdir(__file__) self.assertEqual(cm.exception.errno, errno.ENOTDIR, cm.exception) + @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: None != 'Exception ignored while calling dealloca[83 chars]200>' def test_unraisable(self): # Issue #22836: PyErr_WriteUnraisable() should give sensible reports class BrokenDel: @@ -1693,13 +1740,16 @@ def __del__(self): obj = BrokenDel() with support.catch_unraisable_exception() as cm: + obj_repr = repr(type(obj).__del__) del obj gc_collect() # For PyPy or other GCs. - self.assertEqual(cm.unraisable.object, BrokenDel.__del__) + self.assertEqual(cm.unraisable.err_msg, + f"Exception ignored while calling " + f"deallocator {obj_repr}") self.assertIsNotNone(cm.unraisable.exc_traceback) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_unhandled(self): # Check for sensible reporting of unhandled exceptions for exc_type in (ValueError, BrokenStrException): @@ -1719,7 +1769,7 @@ def test_unhandled(self): self.assertIn("", report) else: self.assertIn("test message", report) - self.assertTrue(report.endswith("\n")) + self.assertEndsWith(report, "\n") @cpython_only # Python built with Py_TRACE_REFS fail with a fatal error in @@ -1859,6 +1909,30 @@ def test_memory_error_in_subinterp(self): rc, _, err = script_helper.assert_python_ok("-c", code) self.assertIn(b'MemoryError', err) + def test_keyerror_context(self): + # Make sure that _PyErr_SetKeyError() chains exceptions + try: + err1 = None + err2 = None + try: + d = {} + try: + raise ValueError("bug") + except Exception as exc: + err1 = exc + d[1] + except Exception as exc: + err2 = exc + + self.assertIsInstance(err1, ValueError) + self.assertIsInstance(err2, KeyError) + self.assertEqual(err2.__context__, err1) + finally: + # Break any potential reference cycle + exc1 = None + exc2 = None + + @cpython_only # Python built with Py_TRACE_REFS fail with a fatal error in # _PyRefchain_Trace() on memory allocation error. @@ -1872,7 +1946,7 @@ def test_exec_set_nomemory_hang(self): # PyLong_FromLong() from returning cached integers, which # don't require a memory allocation. Prepend some dummy code # to artificially increase the instruction index. - warmup_code = "a = list(range(0, 1))\n" * 20 + warmup_code = "a = list(range(0, 1))\n" * 60 user_input = warmup_code + dedent(""" try: import _testcapi @@ -1987,7 +2061,7 @@ def blech(self): class ImportErrorTests(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_attributes(self): # Setting 'name' and 'path' should not be a problem. exc = ImportError('test') @@ -2077,7 +2151,7 @@ class AssertionErrorTests(unittest.TestCase): def tearDown(self): unlink(TESTFN) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON @force_not_colorized def test_assertion_error_location(self): cases = [ @@ -2176,7 +2250,7 @@ def test_assertion_error_location(self): result = run_script(source) self.assertEqual(result[-3:], expected) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON @force_not_colorized def test_multiline_not_highlighted(self): cases = [ @@ -2213,7 +2287,7 @@ def test_multiline_not_highlighted(self): class SyntaxErrorTests(unittest.TestCase): maxDiff = None - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON @force_not_colorized def test_range_of_offsets(self): cases = [ @@ -2305,6 +2379,7 @@ def test_range_of_offsets(self): self.assertIn(expected, err.getvalue()) the_exception = exc + @force_not_colorized def test_subclass(self): class MySyntaxError(SyntaxError): pass @@ -2320,7 +2395,7 @@ class MySyntaxError(SyntaxError): ^^^^^ """, err.getvalue()) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_encodings(self): self.addCleanup(unlink, TESTFN) source = ( @@ -2329,7 +2404,7 @@ def test_encodings(self): ) err = run_script(source.encode('cp437')) self.assertEqual(err[-3], ' "┬ó┬ó┬ó┬ó┬ó┬ó" + f(4, x for x in range(1))') - self.assertEqual(err[-2], ' ^^^^^^^^^^^^^^^^^^^') + self.assertEqual(err[-2], ' ^^^') # Check backwards tokenizer errors source = '# -*- coding: ascii -*-\n\n(\n' @@ -2337,14 +2412,14 @@ def test_encodings(self): self.assertEqual(err[-3], ' (') self.assertEqual(err[-2], ' ^') - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_non_utf8(self): # Check non utf-8 characters self.addCleanup(unlink, TESTFN) err = run_script(b"\x89") self.assertIn("SyntaxError: Non-UTF-8 code starting with '\\x89' in file", err[-1]) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_string_source(self): def try_compile(source): with self.assertRaises(SyntaxError) as cm: @@ -2387,7 +2462,7 @@ def try_compile(source): self.assertEqual(exc.offset, 1) self.assertEqual(exc.end_offset, 12) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_file_source(self): self.addCleanup(unlink, TESTFN) err = run_script('return "ä"') @@ -2450,12 +2525,12 @@ def test_attributes_old_constructor(self): self.assertEqual(error, the_exception.text) self.assertEqual("bad bad", the_exception.msg) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_incorrect_constructor(self): args = ("bad.py", 1, 2) self.assertRaises(TypeError, SyntaxError, "bad bad", args) - args = ("bad.py", 1, 2, 4, 5, 6, 7) + args = ("bad.py", 1, 2, 4, 5, 6, 7, 8) self.assertRaises(TypeError, SyntaxError, "bad bad", args) args = ("bad.py", 1, 2, "abcdefg", 1) @@ -2512,7 +2587,7 @@ def in_except(): pass self.lineno_after_raise(in_except, 4) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_lineno_after_other_except(self): def other_except(): try: @@ -2530,7 +2605,7 @@ def in_named_except(): pass self.lineno_after_raise(in_named_except, 4) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_lineno_in_try(self): def in_try(): try: @@ -2569,7 +2644,7 @@ def after_with(): pass self.lineno_after_raise(after_with, 2) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON def test_missing_lineno_shows_as_none(self): def f(): 1/0 diff --git a/scripts/update_lib/patch_spec.py b/scripts/update_lib/patch_spec.py index 3b68d94b1e4..d35a6351ee9 100644 --- a/scripts/update_lib/patch_spec.py +++ b/scripts/update_lib/patch_spec.py @@ -249,9 +249,17 @@ def _iter_patch_lines( cache = {} # Build per-class set of async method names (for Phase 2 to generate correct override) async_methods: dict[str, set[str]] = {} + # Track class bases for inherited async method lookup + class_bases: dict[str, list[str]] = {} + all_classes = {node.name for node in tree.body if isinstance(node, ast.ClassDef)} for node in tree.body: if isinstance(node, ast.ClassDef): cache[node.name] = node.end_lineno + class_bases[node.name] = [ + base.id + for base in node.bases + if isinstance(base, ast.Name) and base.id in all_classes + ] cls_async: set[str] = set() for item in node.body: if isinstance(item, ast.AsyncFunctionDef): @@ -282,7 +290,19 @@ def _iter_patch_lines( for test_name, specs in tests.items(): decorators = "\n".join(spec.as_decorator() for spec in specs) - is_async = test_name in async_methods.get(cls_name, set()) + # Check current class and ancestors for async method + is_async = False + queue = [cls_name] + visited: set[str] = set() + while queue: + cur = queue.pop(0) + if cur in visited: + continue + visited.add(cur) + if test_name in async_methods.get(cur, set()): + is_async = True + break + queue.extend(class_bases.get(cur, [])) if is_async: patch_lines = f""" {decorators}