mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
Merge pull request #32781 from overleaf/cd-expose-project-files-to-python-output
Cd expose project files to python output GitOrigin-RevId: 410964cc27dd87e795ccb77365dda8344d72f8bf
This commit is contained in:
@@ -5,6 +5,7 @@ import OLButton from '@/shared/components/ol/ol-button'
|
||||
import OLButtonToolbar from '@/shared/components/ol/ol-button-toolbar'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import getMeta from '@/utils/meta'
|
||||
@@ -14,6 +15,7 @@ export default function PythonOutputPane() {
|
||||
const { t } = useTranslation()
|
||||
const { currentDocument, currentDocumentId } = useEditorOpenDocContext()
|
||||
const { pathInFolder } = useFileTreePathContext()
|
||||
const { projectSnapshot } = useProjectContext()
|
||||
const clientRef = useRef<PyodideWorkerClient | null>(null)
|
||||
const currentRequestIdRef = useRef<string | null>(null)
|
||||
const [isReady, setIsReady] = useState(false)
|
||||
@@ -108,7 +110,21 @@ export default function PythonOutputPane() {
|
||||
}
|
||||
}, [currentDocument, currentDocumentId, pathInFolder])
|
||||
|
||||
const handleRun = useCallback(() => {
|
||||
const getLatestProjectFiles = useCallback(async () => {
|
||||
await projectSnapshot.refresh()
|
||||
return projectSnapshot.getDocPaths().reduce(
|
||||
(files, relativePath) => {
|
||||
const content = projectSnapshot.getDocContents(relativePath)
|
||||
if (content !== null) {
|
||||
files.push({ relativePath, content })
|
||||
}
|
||||
return files
|
||||
},
|
||||
[] as { relativePath: string; content: string }[]
|
||||
)
|
||||
}, [projectSnapshot])
|
||||
|
||||
const handleRun = useCallback(async () => {
|
||||
const client = clientRef.current
|
||||
if (!client || !isReady) {
|
||||
return
|
||||
@@ -126,17 +142,19 @@ export default function PythonOutputPane() {
|
||||
currentRequestIdRef.current = requestId
|
||||
setIsRunning(true)
|
||||
|
||||
const isCancelled = () => currentRequestIdRef.current !== requestId
|
||||
|
||||
try {
|
||||
client.runCode(syncFile.content, { requestId, files: [syncFile] })
|
||||
const files = await getLatestProjectFiles()
|
||||
if (isCancelled()) return
|
||||
client.runCode(syncFile.content, { requestId, files })
|
||||
} catch (runError) {
|
||||
if (currentRequestIdRef.current !== requestId) {
|
||||
return
|
||||
}
|
||||
if (isCancelled()) return
|
||||
currentRequestIdRef.current = null
|
||||
setIsRunning(false)
|
||||
setError(formatError(runError))
|
||||
}
|
||||
}, [buildCurrentDocumentSyncFile, isReady])
|
||||
}, [buildCurrentDocumentSyncFile, getLatestProjectFiles, isReady])
|
||||
|
||||
const handleStop = useCallback(() => {
|
||||
const client = clientRef.current
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import React, { FC, PropsWithChildren } from 'react'
|
||||
import PythonOutputPane from '@/features/ide-react/components/editor/python/python-output-pane'
|
||||
import {
|
||||
EditorProviders,
|
||||
projectDefaults,
|
||||
} from '../../../helpers/editor-providers'
|
||||
import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
|
||||
import { ProjectContext } from '@/shared/context/project-context'
|
||||
import { ProjectSnapshot } from '@/infrastructure/project-snapshot'
|
||||
|
||||
const pythonExecutableScript: Record<string, string> = {
|
||||
file_id: 'test-py-doc-id',
|
||||
filename: 'test.py',
|
||||
}
|
||||
|
||||
const FileTreePathProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
return (
|
||||
<FileTreePathContext.Provider
|
||||
value={{
|
||||
pathInFolder: () => pythonExecutableScript.filename,
|
||||
findEntityByPath: () => null,
|
||||
previewByPath: () => null,
|
||||
dirname: () => null,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FileTreePathContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function makeProjectProvider(fileContents: Record<string, string>) {
|
||||
const ProjectProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
const projectSnapshot = {
|
||||
refresh: async () => {},
|
||||
getDocPaths: () => Object.keys(fileContents),
|
||||
getDocContents: (path: string) => fileContents[path] ?? null,
|
||||
} as unknown as ProjectSnapshot
|
||||
|
||||
return (
|
||||
<ProjectContext.Provider
|
||||
value={{
|
||||
projectId: projectDefaults._id,
|
||||
project: projectDefaults,
|
||||
joinProject: () => {},
|
||||
updateProject: () => {},
|
||||
joinedOnce: true,
|
||||
projectSnapshot,
|
||||
tags: [],
|
||||
features: projectDefaults.features,
|
||||
name: projectDefaults.name,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ProjectContext.Provider>
|
||||
)
|
||||
}
|
||||
return ProjectProvider
|
||||
}
|
||||
|
||||
describe('<PythonOutputPane />', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-baseAssetPath', '/__cypress/src/')
|
||||
})
|
||||
|
||||
it('executes a Python script and displays its output', function () {
|
||||
const executablePythonFileContents = "print('hello!')"
|
||||
const projectFiles = {
|
||||
[pythonExecutableScript.filename]: executablePythonFileContents,
|
||||
}
|
||||
const ProjectProvider = makeProjectProvider(projectFiles)
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders
|
||||
scope={{
|
||||
editor: {
|
||||
sharejs_doc: {
|
||||
doc_id: pythonExecutableScript.file_id,
|
||||
getSnapshot: () => executablePythonFileContents,
|
||||
},
|
||||
currentDocumentId: pythonExecutableScript.file_id,
|
||||
openDocName: pythonExecutableScript.filename,
|
||||
},
|
||||
}}
|
||||
providers={{ FileTreePathProvider, ProjectProvider }}
|
||||
>
|
||||
<PythonOutputPane />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Run Python Code' })
|
||||
.should('not.be.disabled')
|
||||
.click()
|
||||
cy.findByText('hello!').should('exist')
|
||||
})
|
||||
|
||||
it('can import and use values from other project Python files', function () {
|
||||
const executablePythonFileContents =
|
||||
'from message import message\nprint(message)'
|
||||
|
||||
const importedPythonFile = {
|
||||
filename: 'message.py',
|
||||
file_contents: "message = 'hello!'",
|
||||
}
|
||||
|
||||
const projectFiles = {
|
||||
[pythonExecutableScript.filename]: executablePythonFileContents,
|
||||
[importedPythonFile.filename]: importedPythonFile.file_contents,
|
||||
}
|
||||
const ProjectProvider = makeProjectProvider(projectFiles)
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders
|
||||
scope={{
|
||||
editor: {
|
||||
sharejs_doc: {
|
||||
doc_id: pythonExecutableScript.file_id,
|
||||
getSnapshot: () => executablePythonFileContents,
|
||||
},
|
||||
currentDocumentId: pythonExecutableScript.file_id,
|
||||
openDocName: pythonExecutableScript.filename,
|
||||
},
|
||||
}}
|
||||
providers={{ FileTreePathProvider, ProjectProvider }}
|
||||
>
|
||||
<PythonOutputPane />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Run Python Code' })
|
||||
.should('not.be.disabled')
|
||||
.click()
|
||||
cy.findByText('hello!').should('exist')
|
||||
})
|
||||
|
||||
it('can import files from different directories relative to the executable script', function () {
|
||||
const executablePythonFileContents = [
|
||||
'from scripts.data_importers.csv_importer import print_data',
|
||||
'print_data()',
|
||||
].join('\n')
|
||||
|
||||
const csvImporterFile = {
|
||||
filename: 'scripts/data_importers/csv_importer.py',
|
||||
file_contents: [
|
||||
'import csv',
|
||||
'',
|
||||
'def print_data():',
|
||||
' with open("food_items.csv", "r") as f:',
|
||||
' reader = csv.reader(f)',
|
||||
' for row in reader:',
|
||||
' print(",".join(row))',
|
||||
].join('\n'),
|
||||
}
|
||||
|
||||
const csvDataFile = {
|
||||
filename: 'food_items.csv',
|
||||
file_contents: 'name,type\nPizza,Italian\nSushi,Japanese\nTacos,Mexican',
|
||||
}
|
||||
|
||||
const projectFiles = {
|
||||
'scripts/test.py': executablePythonFileContents,
|
||||
[csvImporterFile.filename]: csvImporterFile.file_contents,
|
||||
[csvDataFile.filename]: csvDataFile.file_contents,
|
||||
}
|
||||
const ProjectProvider = makeProjectProvider(projectFiles)
|
||||
|
||||
const NestedFileTreePathProvider: FC<PropsWithChildren> = ({
|
||||
children,
|
||||
}) => (
|
||||
<FileTreePathContext.Provider
|
||||
value={{
|
||||
pathInFolder: () => 'scripts/test.py',
|
||||
findEntityByPath: () => null,
|
||||
previewByPath: () => null,
|
||||
dirname: () => null,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FileTreePathContext.Provider>
|
||||
)
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders
|
||||
scope={{
|
||||
editor: {
|
||||
sharejs_doc: {
|
||||
doc_id: pythonExecutableScript.file_id,
|
||||
getSnapshot: () => executablePythonFileContents,
|
||||
},
|
||||
currentDocumentId: pythonExecutableScript.file_id,
|
||||
openDocName: 'test.py',
|
||||
},
|
||||
}}
|
||||
providers={{
|
||||
FileTreePathProvider: NestedFileTreePathProvider,
|
||||
ProjectProvider,
|
||||
}}
|
||||
>
|
||||
<PythonOutputPane />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
||||
cy.findByRole('button', { name: 'Run Python Code' })
|
||||
.should('not.be.disabled')
|
||||
.click()
|
||||
cy.findByText('name,type').should('exist')
|
||||
cy.findByText('Pizza,Italian').should('exist')
|
||||
cy.findByText('Sushi,Japanese').should('exist')
|
||||
cy.findByText('Tacos,Mexican').should('exist')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user