Skip to content

Commit 3579730

Browse files
File system support (#36)
* readFile/writeFile WIP * Added loadModule * added watch module * Tidied up new methods, docs for file system and watch modules * Updated custom module docs * Fixed broken docs link Co-authored-by: James Ansley <[email protected]>
1 parent d8c6652 commit 3579730

File tree

10 files changed

+414
-15
lines changed

10 files changed

+414
-15
lines changed

.prettierrc

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"tabWidth": 2,
33
"singleQuote": true,
4-
"semi": false
4+
"semi": false,
5+
"trailingComma": "none"
56
}

src/hooks/usePython.ts

+58-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useContext, useEffect, useMemo, useRef, useState } from 'react'
22
import {
33
Packages,
44
PythonContext,
5-
suppressedMessages,
5+
suppressedMessages
66
} from '../providers/PythonProvider'
77
import { proxy, Remote, wrap } from 'comlink'
88

@@ -14,6 +14,10 @@ interface Runner {
1414
) => Promise<void>
1515
run: (code: string) => Promise<void>
1616
interruptExecution: () => void
17+
readFile: (name: string) => void
18+
writeFile: (name: string, data: string) => void
19+
mkdir: (name: string) => void
20+
rmdir: (name: string) => void
1721
}
1822

1923
interface UsePythonProps {
@@ -31,12 +35,13 @@ export default function usePython(props?: UsePythonProps) {
3135
const [stderr, setStderr] = useState('')
3236
const [pendingCode, setPendingCode] = useState<string | undefined>()
3337
const [hasRun, setHasRun] = useState(false)
38+
const [watchedModules, setWatchedModules] = useState<Set<string>>(new Set())
3439

3540
const {
3641
packages: globalPackages,
3742
timeout,
3843
lazy,
39-
terminateOnCompletion,
44+
terminateOnCompletion
4045
} = useContext(PythonContext)
4146

4247
const workerRef = useRef<Worker>()
@@ -65,14 +70,14 @@ export default function usePython(props?: UsePythonProps) {
6570
const official = [
6671
...new Set([
6772
...(globalPackages.official ?? []),
68-
...(packages.official ?? []),
69-
]),
73+
...(packages.official ?? [])
74+
])
7075
]
7176
const micropip = [
7277
...new Set([
7378
...(globalPackages.micropip ?? []),
74-
...(packages.micropip ?? []),
75-
]),
79+
...(packages.micropip ?? [])
80+
])
7681
]
7782
return [official, micropip]
7883
}, [globalPackages, packages])
@@ -165,6 +170,18 @@ def run(code, preamble=''):
165170
print()
166171
`
167172

173+
// prettier-ignore
174+
const moduleReloadCode = (modules: Set<string>) => `
175+
import importlib
176+
import sys
177+
${Array.from(modules).map((name) => `
178+
if """${name}""" in sys.modules:
179+
importlib.reload(sys.modules["""${name}"""])
180+
`).join('')}
181+
del importlib
182+
del sys
183+
`
184+
168185
const runPython = async (code: string, preamble = '') => {
169186
// Clear stdout and stderr
170187
setStdout('')
@@ -201,6 +218,9 @@ def run(code, preamble=''):
201218
interruptExecution()
202219
}, timeout)
203220
}
221+
if (watchedModules.size > 0) {
222+
await runnerRef.current.run(moduleReloadCode(watchedModules))
223+
}
204224
await runnerRef.current.run(code)
205225
// eslint-disable-next-line
206226
} catch (error: any) {
@@ -211,6 +231,32 @@ def run(code, preamble=''):
211231
}
212232
}
213233

234+
const readFile = (name: string) => {
235+
return runnerRef.current?.readFile(name)
236+
}
237+
238+
const writeFile = (name: string, data: string) => {
239+
return runnerRef.current?.writeFile(name, data)
240+
}
241+
242+
const mkdir = (name: string) => {
243+
return runnerRef.current?.mkdir(name)
244+
}
245+
246+
const rmdir = (name: string) => {
247+
return runnerRef.current?.rmdir(name)
248+
}
249+
250+
const watchModules = (moduleNames: string[]) => {
251+
setWatchedModules((prev) => new Set([...prev, ...moduleNames]))
252+
}
253+
254+
const unwatchModules = (moduleNames: string[]) => {
255+
setWatchedModules(
256+
(prev) => new Set([...prev].filter((e) => !moduleNames.includes(e)))
257+
)
258+
}
259+
214260
const interruptExecution = () => {
215261
cleanup()
216262
setIsRunning(false)
@@ -236,5 +282,11 @@ def run(code, preamble=''):
236282
isLoading,
237283
isRunning,
238284
interruptExecution,
285+
readFile,
286+
writeFile,
287+
watchModules,
288+
unwatchModules,
289+
mkdir,
290+
rmdir
239291
}
240292
}

src/providers/PythonProvider.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const PythonContext = createContext({
44
packages: {} as Packages,
55
timeout: 0,
66
lazy: false,
7-
terminateOnCompletion: false,
7+
terminateOnCompletion: false
88
})
99

1010
export const suppressedMessages = ['Python initialization complete']
@@ -28,7 +28,7 @@ function PythonProvider(props: PythonProviderProps) {
2828
packages = {},
2929
timeout = 0,
3030
lazy = false,
31-
terminateOnCompletion = false,
31+
terminateOnCompletion = false
3232
} = props
3333

3434
return (
@@ -37,7 +37,7 @@ function PythonProvider(props: PythonProviderProps) {
3737
packages,
3838
timeout,
3939
lazy,
40-
terminateOnCompletion,
40+
terminateOnCompletion
4141
}}
4242
{...props}
4343
/>

src/workers/python-worker.ts

+20-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ interface Pyodide {
55
runPythonAsync: (code: string) => Promise<void>
66
loadPackage: (packages: string[]) => Promise<void>
77
pyimport: (pkg: string) => micropip
8+
FS: {
9+
readFile: (name: string, options: unknown) => void
10+
writeFile: (name: string, data: string, options: unknown) => void
11+
mkdir: (name: string) => void
12+
rmdir: (name: string) => void
13+
}
814
}
915

1016
interface micropip {
@@ -14,7 +20,7 @@ interface micropip {
1420
declare global {
1521
interface Window {
1622
loadPyodide: ({
17-
stdout,
23+
stdout
1824
}: {
1925
stdout: (msg: string) => void
2026
}) => Promise<Pyodide>
@@ -35,7 +41,7 @@ const python = {
3541
packages: string[][]
3642
) {
3743
self.pyodide = await self.loadPyodide({
38-
stdout: (msg: string) => stdout(msg),
44+
stdout: (msg: string) => stdout(msg)
3945
})
4046
if (packages[0].length > 0) {
4147
await self.pyodide.loadPackage(packages[0])
@@ -50,6 +56,18 @@ const python = {
5056
async run(code: string) {
5157
await self.pyodide.runPythonAsync(code)
5258
},
59+
readFile(name: string) {
60+
return self.pyodide.FS.readFile(name, { encoding: 'utf8' })
61+
},
62+
writeFile(name: string, data: string) {
63+
return self.pyodide.FS.writeFile(name, data, { encoding: 'utf8' })
64+
},
65+
mkdir(name: string) {
66+
self.pyodide.FS.mkdir(name)
67+
},
68+
rmdir(name: string) {
69+
self.pyodide.FS.rmdir(name)
70+
}
5371
}
5472

5573
expose(python)
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
sidebar_position: 5
3+
---
4+
5+
import CustomModuleExample from '../../src/components/CustomModuleExample'
6+
7+
# Custom Modules
8+
9+
By default, Python modules are cached. If you intend to make changes to an imported module, it needs to be watched and reloaded.
10+
11+
Create a module using the file system APIs or write the file using Python, [examples here](file-system).
12+
13+
The following example shows this in action. To try it out:
14+
15+
- Click `Write file` and then `Run`
16+
- Make a change to `utils.py` and click `Write file`, note that when you `Run` again, the imported module is unchanged
17+
- Click `Watch` and then observe the module has been reloaded when you `Run` again
18+
19+
<CustomModuleExample />
20+
21+
You find the source code for this example [here](https://github.com/elilambnz/react-py/blob/main/website/src/components/CustomModuleExample.tsx).

website/docs/examples/file-system.md

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
sidebar_position: 4
3+
---
4+
5+
# File System
6+
7+
Some internal Pyodide file system methods are exposed.
8+
9+
For more info, see the [API reference docs](../introduction/api-reference).
10+
11+
```tsx
12+
import { useEffect } from 'react'
13+
import { usePython } from 'react-py'
14+
15+
function Codeblock() {
16+
const { readFile, writeFile, mkdir, rmdir ... } = usePython()
17+
18+
function read() {
19+
const file = readFile('/hello.txt')
20+
console.log(file)
21+
}
22+
23+
function write() {
24+
const data = 'hello world!'
25+
writeFile('/hello.txt', data)
26+
}
27+
28+
function createDir() {
29+
mkdir('lib')
30+
}
31+
32+
function deleteDir() {
33+
rmdir('cruft')
34+
}
35+
36+
...
37+
}
38+
```
39+
40+
You can also use Python to read and write files:
41+
42+
```python
43+
with open("hello.txt", "w") as f:
44+
f.write('hello world!')
45+
46+
with open('hello.txt') as f:
47+
contents = f.read()
48+
print(contents)
49+
```
50+
51+
:::note
52+
Files are not shared between instances, each usage of the `usePython` hook has an independent file system. Files are also not persisted on page reload.
53+
:::

website/docs/examples/playground.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,13 @@ draft: true
88
Used for testing bugs etc.
99

1010
```python
11-
print("hello", end="")
11+
with open("/hello.txt", "w") as fh:
12+
fh.write("hello world!")
13+
print("done")
14+
```
15+
16+
```python
17+
with open("/hello.txt", "r") as fh:
18+
data = fh.read()
19+
print(data)
1220
```

website/docs/introduction/api-reference.md

+34
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,40 @@ True if code is being executed. False if idle.
6363

6464
Can be called to immediately interrupt ongoing execution. Will terminate the running worker and spawn a new one.
6565

66+
### readFile
67+
68+
`(name: string) => void`
69+
70+
Exposes `pyodide.FS.readFile`, encoding is `utf8`.
71+
72+
### writeFile
73+
74+
`(name: string, data: string) => void`
75+
76+
Exposes `pyodide.FS.writeFile`, encoding is `utf8`.
77+
78+
### mkdir
79+
80+
`(name: string) => void`
81+
82+
Exposes `pyodide.FS.mkdir`.
83+
84+
### rmdir
85+
86+
Exposes `pyodide.FS.rmdir`.
87+
88+
### watchModules
89+
90+
`(modules: string[]) => void`
91+
92+
Adds modules to be reloaded before code is run.
93+
94+
### unwatchModules
95+
96+
`(modules: string[]) => void`
97+
98+
Removes modules to be reloaded before code is run.
99+
66100
## Types
67101

68102
### Packages

website/src/components/CodeEditor.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const editorOptions = {
88
enableBasicAutocompletion: true,
99
enableLiveAutocompletion: true,
1010
highlightActiveLine: false,
11-
showPrintMargin: false,
11+
showPrintMargin: false
1212
}
1313

1414
const editorOnLoad = (editor) => {
@@ -38,7 +38,7 @@ export default function CodeEditor(props: CodeEditorProps) {
3838
stderr,
3939
isLoading,
4040
isRunning,
41-
interruptExecution,
41+
interruptExecution
4242
} = usePython()
4343

4444
function run() {

0 commit comments

Comments
 (0)