From 6c9560cd4edb76e5fa48a40acaad53023b2083e0 Mon Sep 17 00:00:00 2001 From: Chris Dryden <117657238+chrisdrydenaltmetric@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:40:24 +0100 Subject: [PATCH] Merge pull request #32943 from overleaf/cd-auto-install-python-packages Auto-install python packages from the executing python script GitOrigin-RevId: e343312d61e1804d927688bf4e0de00b2bdb5382 --- .../tomli-2.2.1-py3-none-any.whl | Bin 0 -> 14257 bytes .../web/cypress/support/webpack.cypress.ts | 15 ++++-- .../editor/python/pyodide-worker-client.ts | 5 ++ .../editor/python/pyodide-worker-messages.ts | 1 + .../editor/python/pyodide.worker.ts | 11 ++++- .../components/editor/python/python-runner.ts | 6 ++- .../context/python-execution-context.tsx | 11 +++-- .../components/python-output-pane.spec.tsx | 43 ++++++++++++++++++ .../unit/editor/pyodide-worker-client.spec.ts | 6 ++- 9 files changed, 86 insertions(+), 12 deletions(-) create mode 100644 services/web/cypress/fixtures/pyodide-packages/tomli-2.2.1-py3-none-any.whl diff --git a/services/web/cypress/fixtures/pyodide-packages/tomli-2.2.1-py3-none-any.whl b/services/web/cypress/fixtures/pyodide-packages/tomli-2.2.1-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..d42f9419ef77c9964d6b6624d0fe77284b5cbc5e GIT binary patch literal 14257 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 a`u{ONQ3e9?A6LWt*|q<~3?Ytx^!^`9mYD|t literal 0 HcmV?d00001 diff --git a/services/web/cypress/support/webpack.cypress.ts b/services/web/cypress/support/webpack.cypress.ts index 77ed1105e1..84a244c5fc 100644 --- a/services/web/cypress/support/webpack.cypress.ts +++ b/services/web/cypress/support/webpack.cypress.ts @@ -10,10 +10,17 @@ const buildConfig = () => { workerPublicPath: '/__cypress/src/', }, devServer: { - static: { - directory: path.join(__dirname, '../../public'), - watch: false, - }, + static: [ + { + directory: path.join(__dirname, '../../public'), + watch: false, + }, + { + directory: path.join(__dirname, '../fixtures/pyodide-packages'), + publicPath: '/pyodide-packages/', + watch: false, + }, + ], port: 3200, }, stats: 'none', diff --git a/services/web/frontend/js/features/ide-react/components/editor/python/pyodide-worker-client.ts b/services/web/frontend/js/features/ide-react/components/editor/python/pyodide-worker-client.ts index c79caeaf8d..c59720a7c1 100644 --- a/services/web/frontend/js/features/ide-react/components/editor/python/pyodide-worker-client.ts +++ b/services/web/frontend/js/features/ide-react/components/editor/python/pyodide-worker-client.ts @@ -30,6 +30,7 @@ export type LifecycleCallback = ( export class PyodideWorkerClient { private worker: Worker private baseAssetPath: string + private packageBaseUrl: string | undefined private createWorker: () => Worker private listening = false private destroyed = false @@ -40,11 +41,13 @@ export class PyodideWorkerClient { constructor(options: { baseAssetPath: string + packageBaseUrl?: string createWorker: () => Worker onOutput?: OutputCallback onLifecycle?: LifecycleCallback }) { this.baseAssetPath = options.baseAssetPath + this.packageBaseUrl = options.packageBaseUrl this.createWorker = options.createWorker this.outputCallback = options.onOutput ?? null this.lifecycleCallback = options.onLifecycle ?? null @@ -54,6 +57,7 @@ export class PyodideWorkerClient { this.queueMessage({ type: 'init', baseAssetPath: this.baseAssetPath, + packageBaseUrl: this.packageBaseUrl, }) } @@ -97,6 +101,7 @@ export class PyodideWorkerClient { this.queueMessage({ type: 'init', baseAssetPath: this.baseAssetPath, + packageBaseUrl: this.packageBaseUrl, }) } diff --git a/services/web/frontend/js/features/ide-react/components/editor/python/pyodide-worker-messages.ts b/services/web/frontend/js/features/ide-react/components/editor/python/pyodide-worker-messages.ts index 8b5f08ad0b..78b621bc2f 100644 --- a/services/web/frontend/js/features/ide-react/components/editor/python/pyodide-worker-messages.ts +++ b/services/web/frontend/js/features/ide-react/components/editor/python/pyodide-worker-messages.ts @@ -15,6 +15,7 @@ export type OutputFileData = { export type InitRequest = { type: 'init' baseAssetPath: string + packageBaseUrl?: string } export type RunCodeRequest = { diff --git a/services/web/frontend/js/features/ide-react/components/editor/python/pyodide.worker.ts b/services/web/frontend/js/features/ide-react/components/editor/python/pyodide.worker.ts index 55a4d61ac3..fc4f3c020f 100644 --- a/services/web/frontend/js/features/ide-react/components/editor/python/pyodide.worker.ts +++ b/services/web/frontend/js/features/ide-react/components/editor/python/pyodide.worker.ts @@ -3,6 +3,7 @@ import path from 'path-browserify' import type { PyodideInterface } from 'pyodide' import type { OutputFileData, + InitRequest, ProjectFileData, PyodideWorkerRequest, RunCodeRequest, @@ -14,6 +15,7 @@ type PyodideModule = typeof import('pyodide') const PROJECT_FS_ROOT = '/project' const PROJECT_FS_PREFIX = `${PROJECT_FS_ROOT}/` const PYODIDE_INDEX_PATH = 'js/libs/pyodide/' +const PYODIDE_CDN_URL = 'https://cdn.jsdelivr.net/pyodide/v' function ensureDirectoryExists(fs: PyodideFS, filePath: string) { const directory = path.dirname(filePath) @@ -49,6 +51,7 @@ function syncProjectFiles(fs: PyodideFS, files: ProjectFileData[]) { } let pyodideModule: PyodideModule | null = null +let packageBaseUrlOverride: string | undefined async function loadPyodideModule(pyodideIndexUrl: string) { const runtimeModuleUrl = `${pyodideIndexUrl}pyodide.mjs` @@ -66,12 +69,14 @@ async function loadPyodideModule(pyodideIndexUrl: string) { } } -async function handleInit(msg: { baseAssetPath: string }) { +async function handleInit(msg: InitRequest) { const pyodideIndexUrl = new URL( PYODIDE_INDEX_PATH, msg.baseAssetPath ).toString() + packageBaseUrlOverride = msg.packageBaseUrl + try { pyodideModule = await loadPyodideModule(pyodideIndexUrl) self.postMessage({ type: 'loaded' }) @@ -109,6 +114,9 @@ async function handleRunCode(msg: RunCodeRequest) { const instance = await pyodideModule.loadPyodide({ env: { MPLBACKEND: 'Agg' }, + packageBaseUrl: + packageBaseUrlOverride ?? + `${PYODIDE_CDN_URL}${pyodideModule.version}/full/`, }) const writtenPaths = new Set() @@ -157,6 +165,7 @@ async function handleRunCode(msg: RunCodeRequest) { return originalWrite.call(fs, ...args) }) as PyodideFS['write'] + await instance.loadPackagesFromImports(msg.code) const result = await instance.runPythonAsync(msg.code) if (result !== undefined) { self.postMessage({ diff --git a/services/web/frontend/js/features/ide-react/components/editor/python/python-runner.ts b/services/web/frontend/js/features/ide-react/components/editor/python/python-runner.ts index 4aa9a12025..693a284afa 100644 --- a/services/web/frontend/js/features/ide-react/components/editor/python/python-runner.ts +++ b/services/web/frontend/js/features/ide-react/components/editor/python/python-runner.ts @@ -43,6 +43,7 @@ export class PythonRunner { readonly fileId: string private client: PyodideWorkerClient | null = null private readonly baseAssetPath: string + private readonly packageBaseUrl: string | undefined private readonly createWorker: () => Worker private readonly getExecutionContext: () => Promise private listeners = new Set() @@ -54,10 +55,12 @@ export class PythonRunner { fileId: string, baseAssetPath: string, getExecutionContext: () => Promise, - createWorker: () => Worker + createWorker: () => Worker, + packageBaseUrl?: string ) { this.fileId = fileId this.baseAssetPath = baseAssetPath + this.packageBaseUrl = packageBaseUrl this.createWorker = createWorker this.getExecutionContext = getExecutionContext } @@ -99,6 +102,7 @@ export class PythonRunner { this.client = new PyodideWorkerClient({ baseAssetPath: this.baseAssetPath, + packageBaseUrl: this.packageBaseUrl, createWorker: this.createWorker, onLifecycle: event => { switch (event.type) { diff --git a/services/web/frontend/js/features/ide-react/context/python-execution-context.tsx b/services/web/frontend/js/features/ide-react/context/python-execution-context.tsx index 417bbd79cd..fc74829cd2 100644 --- a/services/web/frontend/js/features/ide-react/context/python-execution-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/python-execution-context.tsx @@ -37,9 +37,9 @@ export const PythonExecutionContext = createContext< PythonExecutionContextValue | undefined >(undefined) -export const PythonExecutionProvider: FC = ({ - children, -}) => { +export const PythonExecutionProvider: FC< + PropsWithChildren<{ packageBaseUrl?: string }> +> = ({ children, packageBaseUrl }) => { const { openDocs } = useEditorManagerContext() const { projectSnapshot } = useProjectContext() const { pathInFolder } = useFileTreePathContext() @@ -99,13 +99,14 @@ export const PythonExecutionProvider: FC = ({ fileId, baseAssetPathRef.current, () => getExecutionContext(fileId), - createPyodideWorker + createPyodideWorker, + packageBaseUrl ) runner.init() runnersRef.current.set(fileId, runner) return runner }, - [getExecutionContext] + [getExecutionContext, packageBaseUrl] ) useEffect(() => { diff --git a/services/web/test/frontend/features/ide-react/components/python-output-pane.spec.tsx b/services/web/test/frontend/features/ide-react/components/python-output-pane.spec.tsx index 3bf2b3bd2e..655dad4ad0 100644 --- a/services/web/test/frontend/features/ide-react/components/python-output-pane.spec.tsx +++ b/services/web/test/frontend/features/ide-react/components/python-output-pane.spec.tsx @@ -298,4 +298,47 @@ describe('', function () { 'ide-redesign-python-output-pane-line-info' ) }) + + it('can load common python data analysis packages on code execution', function () { + const executablePythonFileContents = [ + 'import tomli', + '', + "print(tomli.loads('greeting = \"hello from tomli\"')['greeting'])", + ].join('\n') + + const projectFiles = { + [pythonExecutableScript.filename]: executablePythonFileContents, + } + const ProjectProvider = makeProjectProvider(projectFiles) + + cy.mount( + executablePythonFileContents, + }, + currentDocumentId: pythonExecutableScript.file_id, + openDocName: pythonExecutableScript.filename, + }, + }} + providers={{ FileTreePathProvider, ProjectProvider }} + > + + + + + ) + + cy.findByRole('button', { name: 'Run Python code' }) + .should('not.be.disabled') + .click() + cy.findByText("ModuleNotFoundError: No module named 'tomli'").should( + 'not.exist' + ) + cy.findByText('hello from tomli').should('exist') + }) }) diff --git a/services/web/test/frontend/features/ide-react/unit/editor/pyodide-worker-client.spec.ts b/services/web/test/frontend/features/ide-react/unit/editor/pyodide-worker-client.spec.ts index be6b684d5d..175b0b5027 100644 --- a/services/web/test/frontend/features/ide-react/unit/editor/pyodide-worker-client.spec.ts +++ b/services/web/test/frontend/features/ide-react/unit/editor/pyodide-worker-client.spec.ts @@ -346,7 +346,11 @@ describe('PyodideWorkerClient', function () { newWorker.emitMessage({ type: 'listening' }) expect(newWorker.postedMessages).to.deep.equal([ - { type: 'init', baseAssetPath: BASE_ASSET_PATH }, + { + type: 'init', + baseAssetPath: BASE_ASSET_PATH, + packageBaseUrl: undefined, + }, ]) })