From 536676337b58aa11c56a7d819ea20908b1b70018 Mon Sep 17 00:00:00 2001 From: Charlotte Van Petegem Date: Mon, 13 Apr 2026 09:59:57 +0200 Subject: [PATCH 1/2] Fix infinite hang during lint when code contains re import --- src/backend/workers/python/papyros/linting.py | 29 +++++++++++++++++++ test/__tests__/state/Runner.test.ts | 22 ++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/backend/workers/python/papyros/linting.py b/src/backend/workers/python/papyros/linting.py index 7db6f4be2..de584d563 100644 --- a/src/backend/workers/python/papyros/linting.py +++ b/src/backend/workers/python/papyros/linting.py @@ -7,6 +7,35 @@ from pylint.lint import Run from pylint.reporters.text import TextReporter +# Workaround for Pyodide + Python 3.12 + astroid 2.15.8: pylint hangs +# indefinitely when analyzing code that imports `re` because astroid +# recursively parses the stdlib `re` package. Short-circuit astroid's +# module loading for `re` (and related stdlib modules) to return a +# tiny synthetic module instead. +from astroid.manager import AstroidManager as _AstroidManager +from astroid.builder import AstroidBuilder as _AstroidBuilder + +_BLOCKED_MODULES = { + "re", + "re._compiler", + "re._parser", + "re._constants", + "re._casefix", + "sre_compile", + "sre_parse", + "sre_constants", +} + +_orig_ast_from_module_name = _AstroidManager.ast_from_module_name + +def _patched_ast_from_module_name(self, modname, context_file=None, use_cache=True): + if modname in _BLOCKED_MODULES: + # Return an empty synthetic module so astroid's inference short-circuits. + return _AstroidBuilder(self).string_build("", modname=modname) + return _orig_ast_from_module_name(self, modname, context_file=context_file, use_cache=use_cache) + +_AstroidManager.ast_from_module_name = _patched_ast_from_module_name + PYLINT_RC_FILE = os.path.abspath("/tmp/papyros/pylint_config.rc") PYLINT_PLUGINS = "pylint_ast_checker" diff --git a/test/__tests__/state/Runner.test.ts b/test/__tests__/state/Runner.test.ts index 8aa832411..277cbbeda 100644 --- a/test/__tests__/state/Runner.test.ts +++ b/test/__tests__/state/Runner.test.ts @@ -49,6 +49,28 @@ const c = a + b;`; expect(papyros.runner.stateMessage).toMatch(/^Code interrupted after /); }); + it("should be able to import re", async () => { + const papyros = new Papyros(); + await papyros.launch(); + papyros.runner.programmingLanguage = ProgrammingLanguage.Python; + papyros.runner.code = "import re\nprint(re.findall(r'\\d+', 'a1 b2 c3'))"; + await papyros.runner.start(); + await waitForPapyrosReady(papyros); + await waitForOutput(papyros); + expect(papyros.runner.state).toBe(RunState.Ready); + expect(papyros.runner.stateMessage).toMatch(/^Code executed in/); + expect(papyros.io.output[0].content).toBe("['1', '2', '3']\n"); + }); + + it("should lint bare import re", async () => { + const papyros = new Papyros(); + await papyros.launch(); + papyros.runner.programmingLanguage = ProgrammingLanguage.Python; + papyros.runner.code = "import re\n"; + const diagnostics = await papyros.runner.lintSource(); + expect(Array.isArray(diagnostics)).toBe(true); + }, 60000); + it("should be able to handle sleep", async () => { const papyros = new Papyros(); await papyros.launch(); From 70ed86bab25499d98004c27efcddbc15f63c3d29 Mon Sep 17 00:00:00 2001 From: Charlotte Van Petegem Date: Mon, 13 Apr 2026 13:41:20 +0200 Subject: [PATCH 2/2] Disable loading of all modules in astroid --- src/backend/workers/python/papyros/linting.py | 30 +++++-------------- .../workers/python/papyros/pylint_config.rc | 2 +- test/__tests__/state/Runner.test.ts | 9 ++++++ 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/backend/workers/python/papyros/linting.py b/src/backend/workers/python/papyros/linting.py index de584d563..2b6c3706b 100644 --- a/src/backend/workers/python/papyros/linting.py +++ b/src/backend/workers/python/papyros/linting.py @@ -7,32 +7,18 @@ from pylint.lint import Run from pylint.reporters.text import TextReporter -# Workaround for Pyodide + Python 3.12 + astroid 2.15.8: pylint hangs -# indefinitely when analyzing code that imports `re` because astroid -# recursively parses the stdlib `re` package. Short-circuit astroid's -# module loading for `re` (and related stdlib modules) to return a -# tiny synthetic module instead. +# Workaround for Pyodide + astroid: pylint hangs indefinitely when astroid +# tries to recursively parse imported modules' ASTs (e.g. re, pandas). +# Since we run in a single-threaded WebAssembly environment, this causes an +# infinite hang. We short-circuit ALL module resolution to return synthetic +# empty modules. This preserves core linting (syntax errors, undefined +# variables, unused imports, style checks, custom checkers) while only +# losing type-inference-based checks on imported symbols. from astroid.manager import AstroidManager as _AstroidManager from astroid.builder import AstroidBuilder as _AstroidBuilder -_BLOCKED_MODULES = { - "re", - "re._compiler", - "re._parser", - "re._constants", - "re._casefix", - "sre_compile", - "sre_parse", - "sre_constants", -} - -_orig_ast_from_module_name = _AstroidManager.ast_from_module_name - def _patched_ast_from_module_name(self, modname, context_file=None, use_cache=True): - if modname in _BLOCKED_MODULES: - # Return an empty synthetic module so astroid's inference short-circuits. - return _AstroidBuilder(self).string_build("", modname=modname) - return _orig_ast_from_module_name(self, modname, context_file=context_file, use_cache=use_cache) + return _AstroidBuilder(self).string_build("", modname=modname) _AstroidManager.ast_from_module_name = _patched_ast_from_module_name diff --git a/src/backend/workers/python/papyros/pylint_config.rc b/src/backend/workers/python/papyros/pylint_config.rc index bffa64ad5..203de407a 100644 --- a/src/backend/workers/python/papyros/pylint_config.rc +++ b/src/backend/workers/python/papyros/pylint_config.rc @@ -43,5 +43,5 @@ const-rgx=[_A-Za-z0-9]{1,30}$ # I0011 Warning locally suppressed using disable-msg # I0012 Warning locally suppressed using disable-msg # old version: disable=I0011,I0012,W0704,W0142,W0212,W0232,W0702,R0201,W0614,R0914,R0912,R0915,R0913,R0904,R0801,C0303,C0111,C0304,R0903,W0141,W0621,C0301,W0631,R0911,C1001 -disable=W0311,W0621,W0622,R0902,R0903,C0111,C0301,C0303,C0304,C0413,I0011 +disable=W0311,W0621,W0622,R0902,R0903,C0111,C0301,C0303,C0304,C0413,I0011,E0401,E0611,E1101 evaluation=max(10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10), 0) diff --git a/test/__tests__/state/Runner.test.ts b/test/__tests__/state/Runner.test.ts index 277cbbeda..1e9a062b2 100644 --- a/test/__tests__/state/Runner.test.ts +++ b/test/__tests__/state/Runner.test.ts @@ -71,6 +71,15 @@ const c = a + b;`; expect(Array.isArray(diagnostics)).toBe(true); }, 60000); + it("should lint code that uses pandas without hanging", async () => { + const papyros = new Papyros(); + await papyros.launch(); + papyros.runner.programmingLanguage = ProgrammingLanguage.Python; + papyros.runner.code = "import pandas as pd\ndf = pd.DataFrame({'a': [1, 2, 3]})\n"; + const diagnostics = await papyros.runner.lintSource(); + expect(Array.isArray(diagnostics)).toBe(true); + }, 60000); + it("should be able to handle sleep", async () => { const papyros = new Papyros(); await papyros.launch();