Skip to content

Commit

Permalink
pyodide version and better install logs (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelcolvin authored Jan 24, 2025
1 parent f9d15ba commit 800638e
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 44 deletions.
18 changes: 14 additions & 4 deletions src/frontend/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,37 @@ export default function () {
const [status, setStatus] = createSignal('Launching Python...')
const [installed, setInstalled] = createSignal('')
const [outputHtml, setOutputHtml] = createSignal('')
const [versions, setVersions] = createSignal('')
let terminalOutput = ''
let worker: Worker
let outputRef!: HTMLPreElement

onMount(async () => {
worker = new Worker()
worker.onmessage = ({ data }: { data: WorkerResponse }) => {
let newTerminalOutput = false
if (data.kind == 'print') {
newTerminalOutput = true
for (const chunk of data.data) {
const arr = new Uint8Array(chunk)
terminalOutput += decoder.decode(arr)
}
} else if (data.kind == 'status') {
setStatus(data.message)
} else if (data.kind == 'error') {
newTerminalOutput = true
terminalOutput += data.message
} else if (data.kind == 'installed') {
setInstalled(data.message.length > 0 ? `Installed dependencies: ${data.message}` : '')
} else {
setStatus(data.message)
setVersions(data.message)
}

if (newTerminalOutput) {
setOutputHtml(ansiConverter.toHtml(escapeHTML(terminalOutput)))
// scrolls to the bottom of the div
outputRef.scrollTop = outputRef.scrollHeight
}
setOutputHtml(ansiConverter.toHtml(escapeHTML(terminalOutput)))
// scrolls to the bottom of the div
outputRef.scrollTop = outputRef.scrollHeight
}
})

Expand All @@ -60,6 +69,7 @@ export default function () {
<div class="status my-5">{status()}</div>
<div class="installed">{installed()}</div>
<pre class="output" innerHTML={outputHtml()} ref={outputRef}></pre>
<div class="status text-right smaller">{versions()}</div>
</div>
</section>
</main>
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/src/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export default function ({ runCode }: EditorProps) {
}

return (
<div class="col">
<div class="col pb-10">
<Show when={files().length} fallback={<div class="loading">loading...</div>}>
<Tabs files={files()} addFile={addFile} changeFile={changeFile} closeFile={closeFile} />
{editorEl}
Expand Down
52 changes: 32 additions & 20 deletions src/frontend/src/install_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
"""
from __future__ import annotations as _annotations
import importlib
import json
import logging
import re
import sys
import traceback
from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path
from typing import Any, TypedDict, Iterable
from typing import Any, TypedDict, Iterable, Literal
import importlib.util

import micropip # noqa
Expand All @@ -24,7 +24,8 @@
__all__ = ('install_deps',)

sys.setrecursionlimit(400)
_already_installed: set[str] = set()
# user a dict here to maintain order
_all_dependencies: dict[str, None] = {}
_logfire_configured = False


Expand All @@ -34,15 +35,27 @@ class File(TypedDict):
activeIndex: int


async def install_deps(files: list[File]) -> str:
@dataclass
class Success:
message: str
kind: Literal['success'] = 'success'


@dataclass
class Error:
message: str
kind: Literal['error'] = 'error'


async def install_deps(files: list[File]) -> Success | Error:
cwd = Path.cwd()
for file in cwd.iterdir():
if file.name != 'run.py' and file.is_file():
file.unlink()
for file in files:
(cwd / file['name']).write_text(file['content'])

dependencies: set[str] = set()
dependencies: dict[str, None] = {}
active: File | None = None
highest = -1
for file in files:
Expand All @@ -55,24 +68,23 @@ async def install_deps(files: list[File]) -> str:
dependencies = _find_pep723_dependencies(active['content'])
if dependencies is None:
dependencies = await _find_import_dependencies(files)
new_dependencies = dependencies - _already_installed
new_dependencies = {dep: None for dep in dependencies if dep not in _all_dependencies}

if new_dependencies:
with _micropip_logging() as file_name:
try:
await micropip.install(new_dependencies, keep_going=True)
await micropip.install(new_dependencies.keys(), keep_going=True)
importlib.invalidate_caches()
except Exception:
message = []
with open(file_name) as f:
message.append(f.read())
message.append(traceback.format_exc())
return json.dumps({'kind': 'error', 'message': '\n'.join(message)})
logs = f.read()
return Error(message=f'{logs}\n{traceback.format_exc()}')

_already_installed.update(new_dependencies)
_all_dependencies.update(new_dependencies)
if 'logfire' in new_dependencies:
_prep_logfire()

return json.dumps({'kind': 'success', 'message': ', '.join(sorted(_already_installed))})
return Success(message=', '.join(_all_dependencies.keys()))


@contextmanager
Expand Down Expand Up @@ -131,7 +143,7 @@ def custom_logfire_configure(*, token: str | None = None, **kwargs):
_logfire_configured = True


def _find_pep723_dependencies(script: str) -> set[str] | None:
def _find_pep723_dependencies(script: str) -> dict[str, None] | None:
"""Extract dependencies from a script with PEP 723 metadata."""
metadata = _read_pep723_metadata(script)
dependencies = metadata.get('dependencies')
Expand All @@ -140,7 +152,7 @@ def _find_pep723_dependencies(script: str) -> set[str] | None:
else:
assert isinstance(dependencies, list), 'dependencies must be a list'
assert all(isinstance(dep, str) for dep in dependencies), 'dependencies must be a list of strings'
return set(dependencies)
return {dep: None for dep in dependencies}


def _read_pep723_metadata(script: str) -> dict[str, Any]:
Expand All @@ -165,9 +177,9 @@ def _read_pep723_metadata(script: str) -> dict[str, Any]:
return {}


async def _find_import_dependencies(files: list[File]) -> set[str]:
async def _find_import_dependencies(files: list[File]) -> dict[str, None]:
"""Find dependencies in imports."""
deps: set[str] = set()
deps: dict[str, None] = {}
for file in files:
try:
imports: list[str] = find_imports(file['content'])
Expand All @@ -178,14 +190,14 @@ async def _find_import_dependencies(files: list[File]) -> set[str]:
return deps


def _find_imports_to_install(imports: list[str]) -> set[str]:
def _find_imports_to_install(imports: list[str]) -> dict[str, None]:
"""Given a list of module names being imported, return package that are not installed."""
to_package_name = pyodide_js._api._import_name_to_package_name.to_py()

to_install: set[str] = set()
to_install: dict[str, None] = {}
for module in imports:
try:
importlib.import_module(module)
except ModuleNotFoundError:
to_install.add(to_package_name.get(module, module))
to_install[to_package_name.get(module, module)] = None
return to_install
12 changes: 11 additions & 1 deletion src/frontend/src/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ aside {
}
.col {
flex: 1;
padding: 5px 5px 10px;
padding: 5px;
overflow-y: scroll;
overflow-x: hidden;
border: 1px solid #aaa;
Expand Down Expand Up @@ -119,6 +119,9 @@ button {
margin-top: 5px;
margin-bottom: 5px;
}
.pb-10 {
padding-bottom: 10px;
}
.status {
color: #2490b5;
}
Expand All @@ -131,6 +134,13 @@ button {
}
.output {
overflow: auto;
flex: 2;
}
.text-right {
text-align: right;
}
.smaller {
font-size: 0.9rem;
}
.tabs {
display: flex;
Expand Down
17 changes: 3 additions & 14 deletions src/frontend/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,9 @@ export interface Print {
kind: 'print'
data: ArrayBuffer[]
}

export interface Error {
kind: 'error'
message: string
}

export interface Installed {
kind: 'installed'
message: string
}

export interface Status {
kind: 'status'
export interface Message {
kind: 'status' | 'error' | 'versions' | 'installed'
message: string
}

export type WorkerResponse = Print | Error | Installed | Status
export type WorkerResponse = Print | Message
12 changes: 8 additions & 4 deletions src/frontend/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,11 @@ self.onmessage = async ({ data }: { data: RunCode }) => {
}
post({ kind: 'status', message: `${msg}Installing dependencies…` })

const [installTime, installedOutput]: [number, string] = await time(
const [installTime, installStatus]: [number, InstallSuccess | InstallError] = await time(
pyodide.runPython('import _install_dependencies; _install_dependencies.install_deps(files)', {
globals: pyodide.toPy({ files: files }),
}),
)
const installStatus: InstallSuccess | InstallError = JSON.parse(installedOutput)
if (installStatus.kind == 'error') {
post({ kind: 'status', message: `${msg}Error occurred` })
post({ kind: 'error', message: installStatus.message })
Expand Down Expand Up @@ -92,12 +91,17 @@ async function getPyodide(): Promise<PyodideInterface> {
const pyodide = await loadPyodide({
indexURL: `https://cdn.jsdelivr.net/pyodide/v${pyodideVersion}/full/`,
})
console.log('Pyodide version', pyodide.version)
const sys = pyodide.pyimport('sys')
const pv = sys.version_info
post({
kind: 'versions',
message: `Python: ${pv.major}.${pv.minor}.${pv.micro} Pyodide: ${pyodide.version}`,
})
setupStreams(pyodide)
await pyodide.loadPackage(['micropip', 'pygments'])

const dirPath = '/tmp/pydantic_run'
pyodide.pyimport('sys').path.append(dirPath)
sys.path.append(dirPath)
const pathlib = pyodide.pyimport('pathlib')
pathlib.Path(dirPath).mkdir()
const moduleName = '_install_dependencies'
Expand Down

0 comments on commit 800638e

Please sign in to comment.