diff --git a/src/backend/workers/python/papyros/linting.py b/src/backend/workers/python/papyros/linting.py index 7db6f4be..2b6c3706 100644 --- a/src/backend/workers/python/papyros/linting.py +++ b/src/backend/workers/python/papyros/linting.py @@ -7,6 +7,21 @@ from pylint.lint import Run from pylint.reporters.text import TextReporter +# 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 + +def _patched_ast_from_module_name(self, modname, context_file=None, use_cache=True): + return _AstroidBuilder(self).string_build("", modname=modname) + +_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/src/backend/workers/python/papyros/pylint_config.rc b/src/backend/workers/python/papyros/pylint_config.rc index bffa64ad..203de407 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 8aa83241..1e9a062b 100644 --- a/test/__tests__/state/Runner.test.ts +++ b/test/__tests__/state/Runner.test.ts @@ -49,6 +49,37 @@ 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 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();