Skip to content

Commit 973459e

Browse files
committed
pythongh-108851: Add support.get_recursion_available()
Add get_recursion_available() and get_recursion_depth() functions to the test.support module. sys.setrecursionlimit() now allows setting the limit to the current recursion depth.
1 parent 03c4080 commit 973459e

6 files changed

Lines changed: 146 additions & 24 deletions

File tree

‎Lib/test/support/__init__.py‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2242,6 +2242,35 @@ def check_disallow_instantiation(testcase, tp, *args, **kwds):
22422242
msg = f"cannot create '{re.escape(qualname)}' instances"
22432243
testcase.assertRaisesRegex(TypeError, msg, tp, *args, **kwds)
22442244

2245+
def get_recursion_depth():
2246+
try:
2247+
import _testinternalcapi
2248+
except ImportError:
2249+
_testinternalcapi = None
2250+
2251+
if _testinternalcapi is not None:
2252+
depth = _testinternalcapi.get_recursion_depth()
2253+
else:
2254+
# Implementation using sys._getframe() and frame.f_back
2255+
try:
2256+
depth = 0
2257+
frame = sys._getframe()
2258+
while frame is not None:
2259+
depth += 1
2260+
frame = frame.f_back
2261+
finally:
2262+
# Break reference cycle
2263+
frame = None
2264+
2265+
# Ignore get_recursion_depth() frame
2266+
return max(depth - 1, 1)
2267+
2268+
def get_recursion_available():
2269+
limit = sys.getrecursionlimit()
2270+
# decrement to ignore get_recursion_available() frame
2271+
depth = get_recursion_depth()
2272+
return limit - depth
2273+
22452274
@contextlib.contextmanager
22462275
def set_recursion_limit(limit):
22472276
"""Temporarily change the recursion limit."""
@@ -2252,15 +2281,15 @@ def set_recursion_limit(limit):
22522281
finally:
22532282
sys.setrecursionlimit(original_limit)
22542283

2255-
def infinite_recursion(max_depth=75):
2284+
def infinite_recursion(max_depth=100):
22562285
"""Set a lower limit for tests that interact with infinite recursions
22572286
(e.g test_ast.ASTHelpers_Test.test_recursion_direct) since on some
22582287
debug windows builds, due to not enough functions being inlined the
22592288
stack size might not handle the default recursion limit (1000). See
22602289
bpo-11105 for details."""
2290+
max_depth = get_recursion_depth() + max_depth
22612291
return set_recursion_limit(max_depth)
22622292

2263-
22642293
def ignore_deprecations_from(module: str, *, like: str) -> object:
22652294
token = object()
22662295
warnings.filterwarnings(

‎Lib/test/test_support.py‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,66 @@ def test_has_strftime_extensions(self):
685685
else:
686686
self.assertTrue(support.has_strftime_extensions)
687687

688+
def test_get_recursion_depth(self):
689+
# test support.get_recursion_depth()
690+
code = textwrap.dedent("""
691+
from test import support
692+
import sys
693+
694+
def check(cond):
695+
if not cond:
696+
raise AssertionError("test failed")
697+
698+
check(support.get_recursion_depth() == 1)
699+
700+
def test_func():
701+
check(support.get_recursion_depth() == 2)
702+
test_func()
703+
704+
def test_recursive(depth, limit):
705+
if depth >= limit:
706+
return
707+
get_depth = support.get_recursion_depth()
708+
print(f"test_recursive: {depth}/{limit}: get_recursion_depth() says {get_depth}")
709+
check(get_depth == depth)
710+
test_recursive(depth + 1, limit)
711+
712+
with support.infinite_recursion():
713+
limit = sys.getrecursionlimit()
714+
test_recursive(2, limit)
715+
""")
716+
script_helper.assert_python_ok("-c", code)
717+
718+
def test_recursion(self):
719+
# test functions:
720+
# - get_recursion_available()
721+
# - infinite_recursion()
722+
def recursive_function(depth):
723+
if depth:
724+
recursive_function(depth - 1)
725+
726+
# test the bare minimum: max_depth=1, only allow 1 frame, one function
727+
# call!
728+
for max_depth in (1, 5, 25, 250):
729+
with support.infinite_recursion(max_depth):
730+
limit = sys.getrecursionlimit()
731+
available = support.get_recursion_available()
732+
try:
733+
recursive_function(available)
734+
# avoid self.assertRaises(RecursionError) which consumes
735+
# more than one frame and so raises RecursionError
736+
try:
737+
recursive_function(available + 1)
738+
except RecursionError:
739+
pass
740+
else:
741+
self.fail("RecursionError was not raised")
742+
except Exception as exc:
743+
# avoid self.subTest() since it consumes more
744+
# than one frame and so raises RecursionError!
745+
raise AssertionError(f"test failed with {max_depth=}, "
746+
f"{limit=}, {available=}") from exc
747+
688748
# XXX -follows a list of untested API
689749
# make_legacy_pyc
690750
# is_resource_enabled

‎Lib/test/test_sys.py‎

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -279,20 +279,28 @@ def test_switchinterval(self):
279279
finally:
280280
sys.setswitchinterval(orig)
281281

282-
def test_recursionlimit(self):
282+
def test_getrecursionlimit(self):
283+
limit = sys.getrecursionlimit()
284+
self.assertIsInstance(limit, int)
285+
self.assertGreater(limit, 1)
286+
283287
self.assertRaises(TypeError, sys.getrecursionlimit, 42)
284-
oldlimit = sys.getrecursionlimit()
285-
self.assertRaises(TypeError, sys.setrecursionlimit)
286-
self.assertRaises(ValueError, sys.setrecursionlimit, -42)
287-
sys.setrecursionlimit(10000)
288-
self.assertEqual(sys.getrecursionlimit(), 10000)
289-
sys.setrecursionlimit(oldlimit)
288+
289+
def test_setrecursionlimit(self):
290+
old_limit = sys.getrecursionlimit()
291+
try:
292+
self.assertRaises(TypeError, sys.setrecursionlimit)
293+
self.assertRaises(ValueError, sys.setrecursionlimit, -42)
294+
sys.setrecursionlimit(10000)
295+
self.assertEqual(sys.getrecursionlimit(), 10000)
296+
finally:
297+
sys.setrecursionlimit(old_limit)
290298

291299
def test_recursionlimit_recovery(self):
292300
if hasattr(sys, 'gettrace') and sys.gettrace():
293301
self.skipTest('fatal error if run with a trace function')
294302

295-
oldlimit = sys.getrecursionlimit()
303+
old_limit = sys.getrecursionlimit()
296304
def f():
297305
f()
298306
try:
@@ -311,35 +319,56 @@ def f():
311319
with self.assertRaises(RecursionError):
312320
f()
313321
finally:
314-
sys.setrecursionlimit(oldlimit)
322+
sys.setrecursionlimit(old_limit)
315323

316324
@test.support.cpython_only
317-
def test_setrecursionlimit_recursion_depth(self):
325+
def test_setrecursionlimit_recursionerror(self):
318326
# Issue #25274: Setting a low recursion limit must be blocked if the
319327
# current recursion depth is already higher than limit.
320328

321-
from _testinternalcapi import get_recursion_depth
322-
323-
def set_recursion_limit_at_depth(depth, limit):
324-
recursion_depth = get_recursion_depth()
325-
if recursion_depth >= depth:
329+
def set_recursion_limit_at_depth(desired_depth, limit):
330+
recursion_depth = support.get_recursion_depth()
331+
if recursion_depth < desired_depth:
332+
set_recursion_limit_at_depth(desired_depth, limit)
333+
else:
326334
with self.assertRaises(RecursionError) as cm:
327335
sys.setrecursionlimit(limit)
328336
self.assertRegex(str(cm.exception),
329337
"cannot set the recursion limit to [0-9]+ "
330338
"at the recursion depth [0-9]+: "
331339
"the limit is too low")
332-
else:
333-
set_recursion_limit_at_depth(depth, limit)
334340

335-
oldlimit = sys.getrecursionlimit()
341+
old_limit = sys.getrecursionlimit()
336342
try:
337343
sys.setrecursionlimit(1000)
338-
339344
for limit in (10, 25, 50, 75, 100, 150, 200):
340-
set_recursion_limit_at_depth(limit, limit)
345+
with self.subTest(limit=limit):
346+
set_recursion_limit_at_depth(limit + 1, limit)
347+
finally:
348+
sys.setrecursionlimit(old_limit)
349+
350+
@test.support.cpython_only
351+
def test_setrecursionlimit_to_depth(self):
352+
def func():
353+
pass
354+
355+
old_limit = sys.getrecursionlimit()
356+
try:
357+
depth = support.get_recursion_depth()
358+
359+
# gh-108851: Corner case: set the limit to current recursion depth
360+
# should be permitted. Calling any Python function raises
361+
# RecursionError! Calling C functions is still ok, like the
362+
# sys.setrecursionlimit() call below.
363+
sys.setrecursionlimit(depth)
364+
try:
365+
func()
366+
except RecursionError:
367+
pass
368+
else:
369+
self.fail("RecursionError was not raised")
341370
finally:
342-
sys.setrecursionlimit(oldlimit)
371+
sys.setrecursionlimit(old_limit)
343372

344373
def test_getwindowsversion(self):
345374
# Raise SkipTest if sys doesn't have getwindowsversion attribute
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:func:`sys.setrecursionlimit` now allows setting the limit to the current
2+
recursion depth. Patch by Victor Stinner.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add ``get_recursion_available()`` and ``get_recursion_depth()`` functions to
2+
the :mod:`test.support` module. Patch by Victor Stinner.

‎Python/sysmodule.c‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1271,7 +1271,7 @@ sys_setrecursionlimit_impl(PyObject *module, int new_limit)
12711271
/* Reject too low new limit if the current recursion depth is higher than
12721272
the new low-water mark. */
12731273
int depth = tstate->py_recursion_limit - tstate->py_recursion_remaining;
1274-
if (depth >= new_limit) {
1274+
if (depth > new_limit) {
12751275
_PyErr_Format(tstate, PyExc_RecursionError,
12761276
"cannot set the recursion limit to %i at "
12771277
"the recursion depth %i: the limit is too low",

0 commit comments

Comments
 (0)