Commit fcf1d003 authored by Tal Einat's avatar Tal Einat Committed by GitHub

bpo-26806: add 30 to the recursion limit in IDLE's shell (GH-13944)

This is done to compensate for the extra stack frames added by
IDLE itself, which cause problems when setting the recursion limit
to low values.

This wraps sys.setrecursionlimit() and sys.getrecursionlimit()
as invisibly as possible.
parent 45bc61b9
......@@ -713,6 +713,10 @@ or ``print`` or ``write`` to sys.stdout or sys.stderr,
IDLE should be started in a command line window. The secondary subprocess
will then be attached to that window for input and output.
The IDLE code running in the execution process adds frames to the call stack
that would not be there otherwise. IDLE wraps ``sys.getrecursionlimit`` and
``sys.setrecursionlimit`` to reduce the effect of the additional stack frames.
If ``sys`` is reset by user code, such as with ``importlib.reload(sys)``,
IDLE's changes are lost and input from the keyboard and output to the screen
will not work correctly.
......
......@@ -3,6 +3,11 @@ Released on 2019-10-20?
======================================
bpo-26806: To compensate for stack frames added by IDLE and avoid
possible problems with low recursion limits, add 30 to limits in the
user code execution process. Subtract 30 when reporting recursion
limits to make this addition mostly transparent.
bpo-37325: Fix tab focus traversal order for help source and custom
run dialogs.
......
This diff is collapsed.
......@@ -6,6 +6,8 @@ from unittest import mock
from test.support import captured_stderr
import io
import sys
class RunTest(unittest.TestCase):
......@@ -260,5 +262,36 @@ class PseudeOutputFilesTest(unittest.TestCase):
self.assertRaises(TypeError, f.close, 1)
class TestSysRecursionLimitWrappers(unittest.TestCase):
def test_bad_setrecursionlimit_calls(self):
run.install_recursionlimit_wrappers()
self.addCleanup(run.uninstall_recursionlimit_wrappers)
f = sys.setrecursionlimit
self.assertRaises(TypeError, f, limit=100)
self.assertRaises(TypeError, f, 100, 1000)
self.assertRaises(ValueError, f, 0)
def test_roundtrip(self):
run.install_recursionlimit_wrappers()
self.addCleanup(run.uninstall_recursionlimit_wrappers)
# check that setting the recursion limit works
orig_reclimit = sys.getrecursionlimit()
self.addCleanup(sys.setrecursionlimit, orig_reclimit)
sys.setrecursionlimit(orig_reclimit + 3)
# check that the new limit is returned by sys.getrecursionlimit()
new_reclimit = sys.getrecursionlimit()
self.assertEqual(new_reclimit, orig_reclimit + 3)
def test_default_recursion_limit_preserved(self):
orig_reclimit = sys.getrecursionlimit()
run.install_recursionlimit_wrappers()
self.addCleanup(run.uninstall_recursionlimit_wrappers)
new_reclimit = sys.getrecursionlimit()
self.assertEqual(new_reclimit, orig_reclimit)
if __name__ == '__main__':
unittest.main(verbosity=2)
......@@ -4,10 +4,12 @@ Simplified, pyshell.ModifiedInterpreter spawns a subprocess with
f'''{sys.executable} -c "__import__('idlelib.run').run.main()"'''
'.run' is needed because __import__ returns idlelib, not idlelib.run.
"""
import functools
import io
import linecache
import queue
import sys
import textwrap
import time
import traceback
import _thread as thread
......@@ -305,6 +307,64 @@ def fix_scaling(root):
font['size'] = round(-0.75*size)
RECURSIONLIMIT_DELTA = 30
def install_recursionlimit_wrappers():
"""Install wrappers to always add 30 to the recursion limit."""
# see: bpo-26806
@functools.wraps(sys.setrecursionlimit)
def setrecursionlimit(*args, **kwargs):
# mimic the original sys.setrecursionlimit()'s input handling
if kwargs:
raise TypeError(
"setrecursionlimit() takes no keyword arguments")
try:
limit, = args
except ValueError:
raise TypeError(f"setrecursionlimit() takes exactly one "
f"argument ({len(args)} given)")
if not limit > 0:
raise ValueError(
"recursion limit must be greater or equal than 1")
return setrecursionlimit.__wrapped__(limit + RECURSIONLIMIT_DELTA)
setrecursionlimit.__doc__ += "\n\n" + textwrap.fill(textwrap.dedent(f"""\
This IDLE wrapper adds {RECURSIONLIMIT_DELTA} to prevent possible
uninterruptible loops.
""").strip())
@functools.wraps(sys.getrecursionlimit)
def getrecursionlimit():
return getrecursionlimit.__wrapped__() - RECURSIONLIMIT_DELTA
getrecursionlimit.__doc__ += "\n\n" + textwrap.fill(textwrap.dedent(f"""\
This IDLE wrapper subtracts {RECURSIONLIMIT_DELTA} to compensate for
the {RECURSIONLIMIT_DELTA} IDLE adds when setting the limit.
""").strip())
# add the delta to the default recursion limit, to compensate
sys.setrecursionlimit(sys.getrecursionlimit() + RECURSIONLIMIT_DELTA)
sys.setrecursionlimit = setrecursionlimit
sys.getrecursionlimit = getrecursionlimit
def uninstall_recursionlimit_wrappers():
"""Uninstall the recursion limit wrappers from the sys module.
IDLE only uses this for tests. Users can import run and call
this to remove the wrapping.
"""
if (
getattr(sys.setrecursionlimit, '__wrapped__', None) and
getattr(sys.getrecursionlimit, '__wrapped__', None)
):
sys.setrecursionlimit = sys.setrecursionlimit.__wrapped__
sys.getrecursionlimit = sys.getrecursionlimit.__wrapped__
sys.setrecursionlimit(sys.getrecursionlimit() - RECURSIONLIMIT_DELTA)
class MyRPCServer(rpc.RPCServer):
def handle_error(self, request, client_address):
......@@ -448,6 +508,8 @@ class MyHandler(rpc.RPCHandler):
# sys.stdin gets changed from within IDLE's shell. See issue17838.
self._keep_stdin = sys.stdin
install_recursionlimit_wrappers()
self.interp = self.get_remote_proxy("interp")
rpc.RPCHandler.getresponse(self, myseq=None, wait=0.05)
......
To compensate for stack frames added by IDLE and avoid possible problems
with low recursion limits, add 30 to limits in the user code execution
process. Subtract 30 when reporting recursion limits to make this addition
mostly transparent.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment