Skip to content

Commit

Permalink
main: Add informative message what do to if RecurrsionError occurs.
Browse files Browse the repository at this point in the history
  • Loading branch information
htgoebel committed Sep 12, 2020
1 parent a755b4e commit 4b0cb36
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 0 deletions.
3 changes: 3 additions & 0 deletions PyInstaller/__main__.py
Expand Up @@ -115,6 +115,9 @@ def run(pyi_args=None, pyi_config=None):

except KeyboardInterrupt:
raise SystemExit("Aborted by user request.")
except RecursionError:
from . import _recursion_to_deep_message
_recursion_to_deep_message.raise_with_msg()


if __name__ == '__main__':
Expand Down
46 changes: 46 additions & 0 deletions PyInstaller/_recursion_to_deep_message.py
@@ -0,0 +1,46 @@
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2020, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------


msg = """
=============================================================
A RecursionError (maximum recursion depth exceeded) occurred.
For working around please follow these instructions
=============================================================
1. In your program's .spec file add this line near the top::
import sys ; sys.setrecursionlimit(sys.getrecursionlimit() * 5)
2. Build your program by running PyInstaller with the .spec file as
argument::
pyinstaller myprog.spec
3. If this fails, you most probably hit an endless recursion in
PyInstaller. Please try to track this down has far as possible,
create a minimal example so we can reproduce and open an issue at
https://github.com/pyinstaller/pyinstaller/issues following the
instructions in the issue template. Many thanks.
Explanation: Python's stack-limit is a safety-belt against endless recursion,
eating up memory. PyInstaller imports modules recursively. If the structure
how modules are imported within your program is awkward, this leads to the
nesting being too deep and hitting Python's stack-limit.
With the default recursion limit (1000), the recursion error occurs at about
115 nested imported, with limit 2000 at about 240, with limit 5000 at about
660.
"""


def raise_with_msg():
raise SystemExit(msg)
1 change: 1 addition & 0 deletions news/4406.core.rst
@@ -0,0 +1 @@
Add informative message what do to if RecurrsionError occurs.
1 change: 1 addition & 0 deletions news/5156.core.rst
@@ -0,0 +1 @@
Add informative message what do to if RecurrsionError occurs.
79 changes: 79 additions & 0 deletions tests/unit/test_recursion_limit.py
@@ -0,0 +1,79 @@
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2020, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

import pytest

from PyInstaller.lib.modulegraph import modulegraph
from PyInstaller import configure
from PyInstaller import __main__ as pyi_main


@pytest.fixture
def large_import_chain(tmpdir):
pkg = tmpdir.join('pkg')
pkg.join('__init__.py').ensure().write('from . import a')
mod = None
for alpha in "abcdefg":
if mod:
# last module of prior sub-pkg imports this package
mod.write("import pkg.%s" % alpha)
subpkg = pkg.join(alpha).mkdir()
subpkg.join('__init__.py').write('from . import %s000' % alpha)
for num in range(250):
# module importing its next sibling
mod = subpkg.join("%s%03i.py" % (alpha, num))
mod.write("from . import %s%03i" % (alpha, num + 1))
script = tmpdir.join('script.py')
script.write('import pkg')
return [str(tmpdir)], str(script)


def test_recursion_to_deep(large_import_chain):
"""
modulegraph is recursive and thus triggers RecursionError if
nesting of imported modules is too deep. This can be worked around
by increasing recursion limit.
With the default recursion limit (1000), the recursion error
occurs at about 115 modules, with limit 2000 (as tested below) at
about 240 modules, with limit 5000 at about 660 modules.
"""
path, script = large_import_chain
mg = modulegraph.ModuleGraph(path)
# Increase recursion limit to 5 times of the default. Given the
# module import chain created above this still should fail.
with pytest.raises(RecursionError):
mg.run_script(str(script))


def test_RecursionError_prints_message(tmpdir, large_import_chain,
monkeypatch):
"""
modulegraph is recursive and thus triggers RecursionError if
nesting of imported modules is too deep. Ensure a respective
informative message is printed if recursion error occurs.
"""
path, script = large_import_chain

default_args = [
'--specpath', str(tmpdir),
'--distpath', str(tmpdir.join("dist")),
'--workpath', str(tmpdir.join("build")),
'--path', str(tmpdir),
]

pyi_args = [script] + default_args
PYI_CONFIG = configure.get_config(upx_dir=None)
PYI_CONFIG['cachedir'] = str(tmpdir)

with pytest.raises(SystemExit) as execinfo:
pyi_main.run(pyi_args, PYI_CONFIG)
assert "sys.setrecursionlimit" in str(execinfo.value)

0 comments on commit 4b0cb36

Please sign in to comment.