mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66966a732e | ||
|
|
ec090922cb | ||
|
|
f769f215b2 | ||
|
|
ffc78ab6a2 | ||
|
|
b609b605f5 | ||
|
|
100a1e4bc1 | ||
|
|
c848061a44 | ||
|
|
2647e78480 | ||
|
|
482d57c27c | ||
|
|
4ce989acf3 | ||
|
|
1e62d0b1fe | ||
|
|
2d3ad0ab2d | ||
|
|
3657492c52 | ||
|
|
a8b8e1de36 | ||
|
|
726009029a | ||
|
|
8b35304ab4 | ||
|
|
9e4cb44d73 | ||
|
|
4bf3651c9a | ||
|
|
67fa31e4ea | ||
|
|
4937a46731 | ||
|
|
b4e9a3093c | ||
|
|
a129be8136 | ||
|
|
eaa6711756 | ||
|
|
b528ba67a9 | ||
|
|
71ad1a40cb | ||
|
|
e433275938 | ||
|
|
87256a662b | ||
|
|
7336ae545e | ||
|
|
d68260c0c7 | ||
|
|
14cc05fb80 |
4
.github/workflows/prepare-release.yml
vendored
4
.github/workflows/prepare-release.yml
vendored
@@ -14,10 +14,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
|
||||
6
.github/workflows/publish-release.yml
vendored
6
.github/workflows/publish-release.yml
vendored
@@ -16,10 +16,10 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
run: tar -cvf ../release.tar * && mv ../release.tar .
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
||||
|
||||
6
.github/workflows/publish-snapshot.yml
vendored
6
.github/workflows/publish-snapshot.yml
vendored
@@ -20,10 +20,10 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
||||
|
||||
6
.github/workflows/publish-unstable.yml
vendored
6
.github/workflows/publish-unstable.yml
vendored
@@ -21,10 +21,10 @@ jobs:
|
||||
working-directory: ./core
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
run: sed -e 's#_PATH_#./#' -e 's#_DOC_VERSION_#latest#' -e 's#_TAG_VERSION_##' -e 's#_VERSION_#latest#' ./public/index.html > ./core/dist/index.html
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
MINICONDA_VERSION: 4.11.0
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 3
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
run: git log --graph -3
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ ci:
|
||||
default_stages: [pre-commit]
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-builtin-literals
|
||||
- id: check-case-conflict
|
||||
@@ -24,8 +24,8 @@ repos:
|
||||
exclude: core/dist|\.min\.js$
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 25.1.0
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 25.9.0
|
||||
hooks:
|
||||
- id: black
|
||||
exclude: core/tests
|
||||
@@ -40,7 +40,7 @@ repos:
|
||||
- tomli
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.11.8
|
||||
rev: v0.13.3
|
||||
hooks:
|
||||
- id: ruff
|
||||
exclude: core/tests
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
ISSUE_TEMPLATE
|
||||
*.min.*
|
||||
package-lock.json
|
||||
bridge/
|
||||
|
||||
@@ -13,15 +13,15 @@ Using PyScript is as simple as:
|
||||
<title>PyScript!</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://pyscript.net/snapshots/2024.9.2/core.css"
|
||||
href="https://pyscript.net/releases/2025.10.1/core.css"
|
||||
/>
|
||||
<script
|
||||
type="module"
|
||||
src="https://pyscript.net/snapshots/2024.9.2/core.js"
|
||||
src="https://pyscript.net/releases/2025.10.1/core.js"
|
||||
></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Use MicroPython to evaluate some Python -->
|
||||
<!-- type mpy (MicroPython) or py (Pyodide) to run some Python -->
|
||||
<script type="mpy" terminal>
|
||||
print("Hello, world!")
|
||||
</script>
|
||||
|
||||
59
bridge/README.md
Normal file
59
bridge/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# @pyscript/bridge
|
||||
|
||||
Import Python utilities directly in JS
|
||||
|
||||
```js
|
||||
// main thread
|
||||
const { ffi: { func_a, func_b } } = await import('./test.js');
|
||||
|
||||
// test.js
|
||||
import bridge from 'https://esm.run/@pyscript/bridge';
|
||||
export const ffi = bridge(import.meta.url, { type: 'mpy', worker: false });
|
||||
|
||||
// test.py
|
||||
def func_a(value):
|
||||
print(f"hello {value}")
|
||||
|
||||
def func_b():
|
||||
import sys
|
||||
return sys.version
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
* **pyscript**: the release version to automatically import if not already available on the page. If no version is provided the *developers' channel* version will be used instead (for developers' purposes only).
|
||||
* **type**: `py` by default to bootstrap *Pyodide*.
|
||||
* **worker**: `true` by default to bootstrap in a *Web Worker*.
|
||||
* **config**: either a *string* or a PyScript compatible config *JS literal* to make it possible to bootstrap files and whatnot. If specified, the `worker` becomes implicitly `true` to avoid multiple configs conflicting on the main thread.
|
||||
* **env**: to share the same environment across multiple modules loaded at different times.
|
||||
|
||||
|
||||
## Tests
|
||||
|
||||
Run `npx mini-coi .` within this folder to then reach out `http://localhost:8080/test/` that will show:
|
||||
|
||||
```
|
||||
PyScript Bridge
|
||||
------------------
|
||||
no config
|
||||
```
|
||||
|
||||
The [test.js](./test/test.js) files uses the following defaults:
|
||||
|
||||
* `pyscript` as `"2025.8.1"`
|
||||
* `type` as `"mpy"`
|
||||
* `worker` as `false`
|
||||
* `config` as `undefined`
|
||||
* `env` as `undefined`
|
||||
|
||||
To test any variant use query string parameters so that `?type=py` will use `py` instead, `worker` will use a worker and `config` will use a basic *config* that brings in another file from the same folder which exposes the version.
|
||||
|
||||
To recap: `http://localhost:8080/test/?type=py&worker&config` will show this instead:
|
||||
|
||||
```
|
||||
PyScript Bridge
|
||||
------------------
|
||||
3.12.7 (main, May 15 2025, 18:47:24) ...
|
||||
```
|
||||
|
||||
Please note when a *config* is used, the `worker` attribute is always `true`.
|
||||
163
bridge/index.js
Normal file
163
bridge/index.js
Normal file
@@ -0,0 +1,163 @@
|
||||
/*! (c) PyScript Development Team */
|
||||
|
||||
const { stringify } = JSON;
|
||||
const { assign, create, entries } = Object;
|
||||
|
||||
const el = (name, props) => assign(document.createElement(name), props);
|
||||
|
||||
/**
|
||||
* Transform a list of keys into a Python dictionary.
|
||||
* `['a', 'b']` => `{ "a": a, "b": b }`
|
||||
* @param {Iterable<string>} keys
|
||||
* @returns {string}
|
||||
*/
|
||||
const dictionary = keys => {
|
||||
const fields = [];
|
||||
for (const key of keys)
|
||||
fields.push(`${stringify(key)}: ${key}`);
|
||||
return `{ ${fields.join(',')} }`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve properly config files relative URLs.
|
||||
* @param {string|Object} config - The configuration to normalize.
|
||||
* @param {string} base - The base URL to resolve relative URLs against.
|
||||
* @returns {string} - The JSON serialized config.
|
||||
*/
|
||||
const normalize = async (config, base) => {
|
||||
if (typeof config === 'string') {
|
||||
base = config;
|
||||
config = await fetch(config).then(res => res.json());
|
||||
}
|
||||
if (typeof config.files === 'object') {
|
||||
const files = {};
|
||||
for (const [key, value] of entries(config.files)) {
|
||||
files[key.startsWith('{') ? key : new URL(key, base)] = value;
|
||||
}
|
||||
config.files = files;
|
||||
}
|
||||
return stringify(config);
|
||||
};
|
||||
|
||||
// this logic is based on a 3 levels cache ...
|
||||
const cache = new Map;
|
||||
|
||||
/**
|
||||
* Return a bridge to a Python module via a `.js` file that has a `.py` alter ego.
|
||||
* @param {string} url - The URL of the JS module that has a Python counterpart.
|
||||
* @param {Object} options - The options for the bridge.
|
||||
* @param {string} [options.type='py'] - The `py` or `mpy` interpreter type, `py` by default.
|
||||
* @param {boolean} [options.worker=true] - Whether to use a worker, `true` by default.
|
||||
* @param {string|Object} [options.config=null] - The configuration for the bridge, `null` by default.
|
||||
* @param {string} [options.env=null] - The optional shared environment to use.
|
||||
* @param {string} [options.serviceWorker=null] - The optional service worker to use as fallback.
|
||||
* @returns {Object} - The bridge to the Python module.
|
||||
*/
|
||||
export default (url, {
|
||||
type = 'py',
|
||||
worker = true,
|
||||
config = null,
|
||||
env = null,
|
||||
serviceWorker = null,
|
||||
pyscript = null,
|
||||
} = {}) => {
|
||||
const { protocol, host, pathname } = new URL(url);
|
||||
const py = pathname.replace(/\.m?js(?:\/\+\w+)?$/, '.py');
|
||||
const file = `${protocol}//${host}${py}`;
|
||||
|
||||
// the first cache is about the desired file in the wild ...
|
||||
if (!cache.has(file)) {
|
||||
// the second cache is about all fields one needs to access out there
|
||||
const exports = new Map;
|
||||
let python;
|
||||
|
||||
cache.set(file, new Proxy(create(null), {
|
||||
get(_, field) {
|
||||
if (!exports.has(field)) {
|
||||
// create an async callback once and always return the same later on
|
||||
exports.set(field, async (...args) => {
|
||||
// the third cache is about reaching lazily the code only once
|
||||
// augmenting its content with exports once and drop it on done
|
||||
if (!python) {
|
||||
// do not await or multiple calls will fetch multiple times
|
||||
// just assign the fetch `Promise` once and return it
|
||||
python = fetch(file).then(async response => {
|
||||
const code = await response.text();
|
||||
// create a unique identifier for the Python context
|
||||
const identifier = pathname.replace(/[^a-zA-Z0-9_]/g, '');
|
||||
const name = `__pyscript_${identifier}${Date.now()}`;
|
||||
// create a Python dictionary with all accessed fields
|
||||
const detail = `{"detail":${dictionary(exports.keys())}}`;
|
||||
// create the arguments for the `dispatchEvent` call
|
||||
const eventArgs = `${stringify(name)},${name}to_ts(${detail})`;
|
||||
// bootstrap the script element type and its attributes
|
||||
const script = el('script', { type, textContent: [
|
||||
'\n', code, '\n',
|
||||
// this is to avoid local scope name clashing
|
||||
`from pyscript import window as ${name}`,
|
||||
`from pyscript.ffi import to_js as ${name}to_ts`,
|
||||
`${name}.dispatchEvent(${name}.CustomEvent.new(${eventArgs}))`,
|
||||
// remove these references even if non-clashing to keep
|
||||
// the local scope clean from undesired entries
|
||||
`del ${name}`,
|
||||
`del ${name}to_ts`,
|
||||
].join('\n') });
|
||||
|
||||
// if config is provided it needs to be a worker to avoid
|
||||
// conflicting with main config on the main thread (just like always)
|
||||
script.toggleAttribute('worker', !!config || !!worker);
|
||||
if (config) {
|
||||
const attribute = await normalize(config, file);
|
||||
script.setAttribute('config', attribute);
|
||||
}
|
||||
|
||||
if (env) script.setAttribute('env', env);
|
||||
if (serviceWorker) script.setAttribute('service-worker', serviceWorker);
|
||||
|
||||
// let PyScript resolve and execute this script
|
||||
document.body.appendChild(script);
|
||||
|
||||
// intercept once the unique event identifier with all exports
|
||||
globalThis.addEventListener(
|
||||
name,
|
||||
event => {
|
||||
resolve(event.detail);
|
||||
script.remove();
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
|
||||
// return a promise that will resolve only once the event
|
||||
// has been emitted and the interpreter evaluated the code
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
|
||||
if (!(Symbol.for('@pyscript/core') in globalThis)) {
|
||||
// bring in PyScript if not available already
|
||||
const cdn = pyscript ?
|
||||
`https://pyscript.net/releases/${pyscript}` :
|
||||
// ⚠️ fallback to developers' channel !!!
|
||||
'https://cdn.jsdelivr.net/npm/@pyscript/core/dist'
|
||||
;
|
||||
document.head.appendChild(
|
||||
el('link', { rel: 'stylesheet', href: `${cdn}/core.css` }),
|
||||
);
|
||||
try { await import(`${cdn}/core.js`) }
|
||||
catch {}
|
||||
}
|
||||
return promise;
|
||||
});
|
||||
}
|
||||
|
||||
// return the `Promise` that will after invoke the exported field
|
||||
return python.then(foreign => foreign[field](...args));
|
||||
});
|
||||
}
|
||||
|
||||
// return the lazily to be resolved once callback to invoke
|
||||
return exports.get(field);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
return cache.get(file);
|
||||
};
|
||||
31
bridge/package.json
Normal file
31
bridge/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@pyscript/bridge",
|
||||
"version": "0.2.2",
|
||||
"description": "A JS based way to use PyScript modules",
|
||||
"type": "module",
|
||||
"module": "./index.js",
|
||||
"unpkg": "./index.js",
|
||||
"jsdelivr": "./jsdelivr.js",
|
||||
"browser": "./index.js",
|
||||
"main": "./index.js",
|
||||
"keywords": [
|
||||
"PyScript",
|
||||
"JS",
|
||||
"Python",
|
||||
"bridge"
|
||||
],
|
||||
"files": [
|
||||
"index.js",
|
||||
"README.md"
|
||||
],
|
||||
"author": "Anaconda Inc.",
|
||||
"license": "APACHE-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/pyscript/pyscript.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/pyscript/pyscript/issues"
|
||||
},
|
||||
"homepage": "https://github.com/pyscript/pyscript#readme"
|
||||
}
|
||||
31
bridge/test/index.html
Normal file
31
bridge/test/index.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>PyScript Bridge</title>
|
||||
<style>body { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; }</style>
|
||||
<!-- for local testing purpose only-->
|
||||
<script type="importmap">{"imports":{"https://esm.run/@pyscript/bridge":"../index.js"}}</script>
|
||||
<script type="module">
|
||||
const { ffi: { test_func, test_other, version } } = await import('./test.js');
|
||||
|
||||
console.time("⏱️ first invoke");
|
||||
const result = await test_func("PyScript Bridge");
|
||||
console.timeEnd("⏱️ first invoke");
|
||||
|
||||
document.body.append(
|
||||
Object.assign(
|
||||
document.createElement("h3"),
|
||||
{ textContent: result },
|
||||
),
|
||||
document.createElement("hr"),
|
||||
await version(),
|
||||
);
|
||||
|
||||
console.time("⏱️ other invokes");
|
||||
await test_other("🐍");
|
||||
console.timeEnd("⏱️ other invokes");
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
||||
40
bridge/test/remote/index.html
Normal file
40
bridge/test/remote/index.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>PyScript Bridge</title>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"https://esm.run/@pyscript/bridge": "https://esm.run/@pyscript/bridge@latest",
|
||||
"https://esm.run/@pyscript/bridge/test/test.js": "https://esm.run/@pyscript/bridge@latest/test/test.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>body { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; }</style>
|
||||
<link rel="stylesheet" href="https://pyscript.net/releases/2025.5.1/core.css" />
|
||||
<script type="module" src="https://pyscript.net/releases/2025.5.1/core.js"></script>
|
||||
<script type="module">
|
||||
const cdn_test = 'https://esm.run/@pyscript/bridge/test/test.js';
|
||||
const { ffi: { test_func, test_other, version } } = await import(cdn_test);
|
||||
|
||||
console.time("⏱️ first invoke");
|
||||
const result = await test_func("PyScript Bridge");
|
||||
console.timeEnd("⏱️ first invoke");
|
||||
|
||||
document.body.append(
|
||||
Object.assign(
|
||||
document.createElement("h3"),
|
||||
{ textContent: result },
|
||||
),
|
||||
document.createElement("hr"),
|
||||
await version(),
|
||||
);
|
||||
|
||||
console.time("⏱️ other invokes");
|
||||
await test_other("🐍");
|
||||
console.timeEnd("⏱️ other invokes");
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
||||
5
bridge/test/sys_version.py
Normal file
5
bridge/test/sys_version.py
Normal file
@@ -0,0 +1,5 @@
|
||||
import sys
|
||||
|
||||
|
||||
def version():
|
||||
return sys.version
|
||||
18
bridge/test/test.js
Normal file
18
bridge/test/test.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import bridge from "https://esm.run/@pyscript/bridge";
|
||||
|
||||
// for local testing purpose only
|
||||
const { searchParams } = new URL(location.href);
|
||||
|
||||
// the named (or default) export for test.py
|
||||
export const ffi = bridge(import.meta.url, {
|
||||
pyscript: "2025.8.1",
|
||||
env: searchParams.get("env"),
|
||||
type: searchParams.get("type") || "mpy",
|
||||
worker: searchParams.has("worker"),
|
||||
config: searchParams.has("config") ?
|
||||
({
|
||||
files: {
|
||||
"./sys_version.py": "./sys_version.py",
|
||||
},
|
||||
}) : undefined,
|
||||
});
|
||||
22
bridge/test/test.py
Normal file
22
bridge/test/test.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from pyscript import config, RUNNING_IN_WORKER
|
||||
|
||||
type = config["type"]
|
||||
print(f"{type}-script", RUNNING_IN_WORKER and "worker" or "main")
|
||||
|
||||
|
||||
def test_func(message):
|
||||
print("Python", message)
|
||||
return message
|
||||
|
||||
|
||||
def test_other(message):
|
||||
print("Python", message)
|
||||
return message
|
||||
|
||||
|
||||
def version():
|
||||
try:
|
||||
from sys_version import version
|
||||
except ImportError:
|
||||
version = lambda: "no config"
|
||||
return version()
|
||||
846
core/package-lock.json
generated
846
core/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@pyscript/core",
|
||||
"version": "0.6.53",
|
||||
"version": "0.7.4",
|
||||
"type": "module",
|
||||
"description": "PyScript",
|
||||
"module": "./index.js",
|
||||
@@ -42,7 +42,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"server": "echo \"➡️ TESTS @ $(tput bold)http://localhost:8080/tests/$(tput sgr0)\"; npx static-handler --coi .",
|
||||
"build": "export ESLINT_USE_FLAT_CONFIG=true;npm run build:3rd-party && npm run build:stdlib && npm run build:plugins && npm run build:core && npm run build:tests-index && if [ -z \"$NO_MIN\" ]; then eslint src/ && npm run ts && npm run test:integration; fi",
|
||||
"build": "export ESLINT_USE_FLAT_CONFIG=true;npm run build:3rd-party && npm run build:stdlib && npm run build:plugins && npm run build:core && npm run build:tests-index && if [ -z \"$NO_MIN\" ]; then eslint src/ && npm run test:integration; fi",
|
||||
"build:core": "rm -rf dist && rollup --config rollup/core.config.js && cp src/3rd-party/*.css dist/",
|
||||
"build:flatted": "node rollup/flatted.cjs",
|
||||
"build:plugins": "node rollup/plugins.cjs",
|
||||
@@ -67,40 +67,40 @@
|
||||
"dependencies": {
|
||||
"@ungap/with-resolvers": "^0.1.0",
|
||||
"@webreflection/idb-map": "^0.3.2",
|
||||
"@webreflection/utils": "^0.1.0",
|
||||
"@webreflection/utils": "^0.1.1",
|
||||
"add-promise-listener": "^0.1.3",
|
||||
"basic-devtools": "^0.1.6",
|
||||
"polyscript": "^0.17.20",
|
||||
"polyscript": "^0.19.6",
|
||||
"sticky-module": "^0.1.1",
|
||||
"to-json-callback": "^0.1.1",
|
||||
"type-checked-collections": "^0.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/commands": "^6.8.1",
|
||||
"@codemirror/commands": "^6.9.0",
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/language": "^6.11.0",
|
||||
"@codemirror/language": "^6.11.3",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.36.8",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.3",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@codemirror/view": "^6.38.6",
|
||||
"@playwright/test": "^1.56.1",
|
||||
"@rollup/plugin-commonjs": "^28.0.8",
|
||||
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@webreflection/toml-j0.4": "^1.1.4",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"bun": "^1.2.13",
|
||||
"bun": "^1.3.0",
|
||||
"chokidar": "^4.0.3",
|
||||
"codedent": "^0.1.2",
|
||||
"codemirror": "^6.0.1",
|
||||
"eslint": "^9.27.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"eslint": "^9.38.0",
|
||||
"flatted": "^3.3.3",
|
||||
"rollup": "^4.41.0",
|
||||
"rollup": "^4.52.5",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"rollup-plugin-string": "^3.0.0",
|
||||
"static-handler": "^0.5.3",
|
||||
"string-width": "^7.2.0",
|
||||
"typescript": "^5.8.3",
|
||||
"string-width": "^8.1.0",
|
||||
"typescript": "^5.9.3",
|
||||
"xterm-readline": "^1.1.2"
|
||||
},
|
||||
"repository": {
|
||||
|
||||
2
core/src/3rd-party/xterm.css
vendored
2
core/src/3rd-party/xterm.css
vendored
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Minified by jsDelivr using clean-css v5.3.2.
|
||||
* Minified by jsDelivr using clean-css v5.3.3.
|
||||
* Original file: /npm/@xterm/xterm@5.5.0/css/xterm.css
|
||||
*
|
||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||
|
||||
@@ -70,6 +70,7 @@ for (const [TYPE] of TYPES) {
|
||||
|
||||
let config,
|
||||
type,
|
||||
parser,
|
||||
pyElement,
|
||||
pyConfigs = $$(`${TYPE}-config`),
|
||||
attrConfigs = $$(
|
||||
@@ -92,9 +93,11 @@ for (const [TYPE] of TYPES) {
|
||||
[pyElement] = pyConfigs;
|
||||
config = pyElement.getAttribute("src") || pyElement.textContent;
|
||||
type = pyElement.getAttribute("type");
|
||||
parser = pyElement.getAttribute("config-parser");
|
||||
} else if (attrConfigs.length) {
|
||||
[pyElement, ...attrConfigs] = attrConfigs;
|
||||
config = pyElement.getAttribute("config");
|
||||
parser = pyElement.getAttribute("config-parser");
|
||||
// throw an error if dirrent scripts use different configs
|
||||
if (
|
||||
attrConfigs.some((el) => el.getAttribute("config") !== config)
|
||||
@@ -120,9 +123,12 @@ for (const [TYPE] of TYPES) {
|
||||
}
|
||||
} else if (toml || type === "toml") {
|
||||
try {
|
||||
const { parse } = await import(
|
||||
/* webpackIgnore: true */ "./3rd-party/toml.js"
|
||||
);
|
||||
const module = parser
|
||||
? await import(parser)
|
||||
: await import(
|
||||
/* webpackIgnore: true */ "./3rd-party/toml.js"
|
||||
);
|
||||
const parse = module.parse || module.default;
|
||||
parsed = parse(text);
|
||||
} catch (e) {
|
||||
error = syntaxError("TOML", url, e);
|
||||
@@ -154,6 +160,9 @@ for (const [TYPE] of TYPES) {
|
||||
return await Promise.all(toBeAwaited);
|
||||
};
|
||||
|
||||
if (Number.isSafeInteger(parsed?.experimental_ffi_timeout))
|
||||
globalThis.reflected_ffi_timeout = parsed?.experimental_ffi_timeout;
|
||||
|
||||
configs.set(TYPE, { config: parsed, configURL, plugins, error });
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ const getRelatedScript = (target, type) => {
|
||||
return editor?.parentNode?.previousElementSibling;
|
||||
};
|
||||
|
||||
async function execute({ currentTarget }) {
|
||||
async function execute({ currentTarget, script }) {
|
||||
const { env, pySrc, outDiv } = this;
|
||||
const hasRunButton = !!currentTarget;
|
||||
|
||||
@@ -91,14 +91,13 @@ async function execute({ currentTarget }) {
|
||||
// creation and destruction of editors on the fly
|
||||
if (hasRunButton) {
|
||||
for (const type of TYPES.keys()) {
|
||||
const script = getRelatedScript(currentTarget, type);
|
||||
if (script) {
|
||||
defineProperties(script, { xworker: { value: xworker } });
|
||||
break;
|
||||
}
|
||||
script = getRelatedScript(currentTarget, type);
|
||||
if (script) break;
|
||||
}
|
||||
}
|
||||
|
||||
defineProperties(script, { xworker: { value: xworker } });
|
||||
|
||||
const { sync } = xworker;
|
||||
const { promise, resolve } = withResolvers();
|
||||
envs.set(env, promise);
|
||||
@@ -157,6 +156,20 @@ async function execute({ currentTarget }) {
|
||||
});
|
||||
}
|
||||
|
||||
const replaceScript = (script, type) => {
|
||||
script.xworker?.terminate();
|
||||
const clone = script.cloneNode(true);
|
||||
clone.type = `${type}-editor`;
|
||||
const editor = editors.get(script);
|
||||
if (editor) {
|
||||
const content = editor.state.doc.toString();
|
||||
clone.textContent = content;
|
||||
editors.delete(script);
|
||||
script.nextElementSibling.remove();
|
||||
}
|
||||
script.replaceWith(clone);
|
||||
};
|
||||
|
||||
const makeRunButton = (handler, type) => {
|
||||
const runButton = document.createElement("button");
|
||||
runButton.className = `absolute ${type}-editor-run-button`;
|
||||
@@ -169,15 +182,25 @@ const makeRunButton = (handler, type) => {
|
||||
) {
|
||||
const script = getRelatedScript(runButton, type);
|
||||
if (script) {
|
||||
const editor = editors.get(script);
|
||||
const content = editor.state.doc.toString();
|
||||
const clone = script.cloneNode(true);
|
||||
clone.type = `${type}-editor`;
|
||||
clone.textContent = content;
|
||||
script.xworker.terminate();
|
||||
script.nextElementSibling.remove();
|
||||
script.replaceWith(clone);
|
||||
editors.delete(script);
|
||||
const env = script.getAttribute("env");
|
||||
// remove the bootstrapped env which could be one or shared
|
||||
if (env) {
|
||||
for (const [key, value] of TYPES) {
|
||||
if (key === type) {
|
||||
configs.delete(`${value}-${env}`);
|
||||
envs.delete(`${value}-${env}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// lonley script without setup node should be replaced
|
||||
if (script.xworker) replaceScript(script, type);
|
||||
// all scripts sharing the same env should be replaced
|
||||
else {
|
||||
const sel = `script[type^="${type}-editor"][env="${env}"]`;
|
||||
for (const script of document.querySelectorAll(sel))
|
||||
replaceScript(script, type);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -365,7 +388,7 @@ const init = async (script, type, interpreter) => {
|
||||
};
|
||||
|
||||
if (isSetup) {
|
||||
await context.handleEvent({ currentTarget: null });
|
||||
await context.handleEvent({ currentTarget: null, script });
|
||||
notifyEditor();
|
||||
return;
|
||||
}
|
||||
@@ -403,16 +426,17 @@ const init = async (script, type, interpreter) => {
|
||||
// preserve user indentation, if any
|
||||
const indentation = /^([ \t]+)/m.test(doc) ? RegExp.$1 : " ";
|
||||
|
||||
const listener = () => runButton.click();
|
||||
const listener = () => !runButton.click();
|
||||
const editor = new EditorView({
|
||||
extensions: [
|
||||
indentUnit.of(indentation),
|
||||
new Compartment().of(python()),
|
||||
keymap.of([
|
||||
...defaultKeymap,
|
||||
{ key: "Ctrl-Enter", run: listener, preventDefault: true },
|
||||
{ key: "Cmd-Enter", run: listener, preventDefault: true },
|
||||
{ key: "Shift-Enter", run: listener, preventDefault: true },
|
||||
// Consider removing defaultKeymap as likely redundant with basicSetup
|
||||
...defaultKeymap,
|
||||
// @see https://codemirror.net/examples/tab/
|
||||
indentWithTab,
|
||||
]),
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -4,6 +4,7 @@ import io
|
||||
import re
|
||||
|
||||
from pyscript.magic_js import current_target, document, window
|
||||
from pyscript.ffi import is_none
|
||||
|
||||
_MIME_METHODS = {
|
||||
"savefig": "image/png",
|
||||
@@ -105,13 +106,13 @@ def _format_mime(obj):
|
||||
else:
|
||||
output = _eval_formatter(obj, method)
|
||||
|
||||
if output is None:
|
||||
if is_none(output):
|
||||
continue
|
||||
if mime_type not in _MIME_RENDERERS:
|
||||
not_available.append(mime_type)
|
||||
continue
|
||||
break
|
||||
if output is None:
|
||||
if is_none(output):
|
||||
if not_available:
|
||||
window.console.warn(
|
||||
f"Rendered object requested unavailable MIME renderers: {not_available}"
|
||||
@@ -135,7 +136,7 @@ def _write(element, value, append=False):
|
||||
element.append(out_element)
|
||||
else:
|
||||
out_element = element.lastElementChild
|
||||
if out_element is None:
|
||||
if is_none(out_element):
|
||||
out_element = element
|
||||
|
||||
if mime_type in ("application/javascript", "text/html"):
|
||||
@@ -146,7 +147,7 @@ def _write(element, value, append=False):
|
||||
|
||||
|
||||
def display(*values, target=None, append=True):
|
||||
if target is None:
|
||||
if is_none(target):
|
||||
target = current_target()
|
||||
elif not isinstance(target, str):
|
||||
msg = f"target must be str or None, not {target.__class__.__name__}"
|
||||
@@ -162,7 +163,7 @@ def display(*values, target=None, append=True):
|
||||
element = document.getElementById(target)
|
||||
|
||||
# If target cannot be found on the page, a ValueError is raised
|
||||
if element is None:
|
||||
if is_none(element):
|
||||
msg = f"Invalid selector with id={target}. Cannot be found in the page."
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
@@ -92,60 +92,26 @@ def when(target, *args, **kwargs):
|
||||
elements = selector if isinstance(selector, list) else [selector]
|
||||
|
||||
def decorator(func):
|
||||
if config["type"] == "mpy": # Is MicroPython?
|
||||
sig = inspect.signature(func)
|
||||
if sig.parameters:
|
||||
if is_awaitable(func):
|
||||
|
||||
async def wrapper(event):
|
||||
return await func(event)
|
||||
|
||||
else:
|
||||
wrapper = func
|
||||
else:
|
||||
# Function doesn't receive events.
|
||||
if is_awaitable(func):
|
||||
|
||||
async def wrapper(*args, **kwargs):
|
||||
"""
|
||||
This is a very ugly hack to get micropython working because
|
||||
`inspect.signature` doesn't exist. It may be actually better
|
||||
to not try any magic for now and raise the error.
|
||||
"""
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
except TypeError as e:
|
||||
if "takes" in str(e) and "positional arguments" in str(e):
|
||||
return await func()
|
||||
raise
|
||||
return await func()
|
||||
|
||||
else:
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
"""
|
||||
This is a very ugly hack to get micropython working because
|
||||
`inspect.signature` doesn't exist. It may be actually better
|
||||
to not try any magic for now and raise the error.
|
||||
"""
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
except TypeError as e:
|
||||
if "takes" in str(e) and "positional arguments" in str(e):
|
||||
return func()
|
||||
raise
|
||||
|
||||
else:
|
||||
sig = inspect.signature(func)
|
||||
if sig.parameters:
|
||||
if is_awaitable(func):
|
||||
|
||||
async def wrapper(event):
|
||||
return await func(event)
|
||||
|
||||
else:
|
||||
wrapper = func
|
||||
else:
|
||||
# Function doesn't receive events.
|
||||
if is_awaitable(func):
|
||||
|
||||
async def wrapper(*args, **kwargs):
|
||||
return await func()
|
||||
|
||||
else:
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
return func()
|
||||
return func()
|
||||
|
||||
wrapper = wraps(func)(wrapper)
|
||||
if isinstance(target, Event):
|
||||
|
||||
@@ -2,8 +2,10 @@ try:
|
||||
import js
|
||||
from pyodide.ffi import create_proxy as _cp
|
||||
from pyodide.ffi import to_js as _py_tjs
|
||||
from pyodide.ffi import jsnull
|
||||
|
||||
from_entries = js.Object.fromEntries
|
||||
is_none = lambda value: value is None or value is jsnull
|
||||
|
||||
def _tjs(value, **kw):
|
||||
if not hasattr(kw, "dict_converter"):
|
||||
@@ -13,6 +15,34 @@ try:
|
||||
except:
|
||||
from jsffi import create_proxy as _cp
|
||||
from jsffi import to_js as _tjs
|
||||
import js
|
||||
|
||||
jsnull = js.Object.getPrototypeOf(js.Object.prototype)
|
||||
is_none = lambda value: value is None or value is jsnull
|
||||
|
||||
create_proxy = _cp
|
||||
to_js = _tjs
|
||||
|
||||
try:
|
||||
from polyscript import ffi as _ffi
|
||||
|
||||
direct = _ffi.direct
|
||||
gather = _ffi.gather
|
||||
query = _ffi.query
|
||||
|
||||
def assign(source, *args):
|
||||
for arg in args:
|
||||
_ffi.assign(source, to_js(arg))
|
||||
return source
|
||||
|
||||
except:
|
||||
import js
|
||||
|
||||
_assign = js.Object.assign
|
||||
|
||||
direct = lambda source: source
|
||||
|
||||
def assign(source, *args):
|
||||
for arg in args:
|
||||
_assign(source, to_js(arg))
|
||||
return source
|
||||
|
||||
@@ -49,6 +49,28 @@ async def mount(path, mode="readwrite", root="", id="pyscript"):
|
||||
mounted[path] = await interpreter.mountNativeFS(path, handler)
|
||||
|
||||
|
||||
async def revoke(path, id="pyscript"):
|
||||
from _pyscript import fs, interpreter
|
||||
from pyscript.magic_js import (
|
||||
RUNNING_IN_WORKER,
|
||||
sync,
|
||||
)
|
||||
|
||||
uid = f"{path}@{id}"
|
||||
|
||||
if RUNNING_IN_WORKER:
|
||||
had = sync.deleteFSHandler(uid)
|
||||
else:
|
||||
had = await fs.idb.has(uid)
|
||||
if had:
|
||||
had = await fs.idb.delete(uid)
|
||||
|
||||
if had:
|
||||
interpreter._module.FS.unmount(path)
|
||||
|
||||
return had
|
||||
|
||||
|
||||
async def sync(path):
|
||||
await mounted[path].syncfs()
|
||||
|
||||
|
||||
@@ -67,7 +67,11 @@ if RUNNING_IN_WORKER:
|
||||
|
||||
else:
|
||||
import _pyscript
|
||||
from _pyscript import PyWorker, js_import
|
||||
from _pyscript import PyWorker as _PyWorker, js_import
|
||||
from pyscript.ffi import to_js
|
||||
|
||||
def PyWorker(url, **kw):
|
||||
return _PyWorker(url, to_js(kw))
|
||||
|
||||
window = globalThis
|
||||
document = globalThis.document
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from polyscript import storage as _storage
|
||||
from pyscript.flatted import parse as _parse
|
||||
from pyscript.flatted import stringify as _stringify
|
||||
from pyscript.ffi import is_none
|
||||
|
||||
|
||||
# convert a Python value into an IndexedDB compatible entry
|
||||
def _to_idb(value):
|
||||
if value is None:
|
||||
if is_none(value):
|
||||
return _stringify(["null", 0])
|
||||
if isinstance(value, (bool, float, int, str, list, dict, tuple)):
|
||||
return _stringify(["generic", value])
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# from __future__ import annotations # CAUTION: This is not supported in MicroPython.
|
||||
|
||||
from pyscript import document, when, Event # noqa: F401
|
||||
from pyscript.ffi import create_proxy
|
||||
from pyscript.ffi import create_proxy, is_none
|
||||
|
||||
|
||||
def wrap_dom_element(dom_element):
|
||||
@@ -68,8 +68,10 @@ class Element:
|
||||
If `dom_element` is None we are being called to *create* a new element.
|
||||
Otherwise, we are being called to *wrap* an existing DOM element.
|
||||
"""
|
||||
self._dom_element = dom_element or document.createElement(
|
||||
type(self).get_tag_name()
|
||||
self._dom_element = (
|
||||
document.createElement(type(self).get_tag_name())
|
||||
if is_none(dom_element)
|
||||
else dom_element
|
||||
)
|
||||
|
||||
# HTML on_events attached to the element become pyscript.Event instances.
|
||||
@@ -124,6 +126,11 @@ class Element:
|
||||
# Element instance via `for_`).
|
||||
if name.endswith("_"):
|
||||
name = name[:-1] # noqa: FURB188 No str.removesuffix() in MicroPython.
|
||||
if name == "for":
|
||||
# The `for` attribute is a special case as it is a keyword in both
|
||||
# Python and JavaScript.
|
||||
# We need to get it from the underlying DOM element as `htmlFor`.
|
||||
name = "htmlFor"
|
||||
return getattr(self._dom_element, name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
@@ -142,6 +149,11 @@ class Element:
|
||||
# Element instance via `for_`).
|
||||
if name.endswith("_"):
|
||||
name = name[:-1] # noqa: FURB188 No str.removesuffix() in MicroPython.
|
||||
if name == "for":
|
||||
# The `for` attribute is a special case as it is a keyword in both
|
||||
# Python and JavaScript.
|
||||
# We need to set it on the underlying DOM element as `htmlFor`.
|
||||
name = "htmlFor"
|
||||
|
||||
if name.startswith("on_"):
|
||||
# Ensure on-events are cached in the _on_events dict if the
|
||||
@@ -185,7 +197,7 @@ class Element:
|
||||
@property
|
||||
def parent(self):
|
||||
"""Return the element's `parent `Element`."""
|
||||
if self._dom_element.parentElement is None:
|
||||
if is_none(self._dom_element.parentElement):
|
||||
return None
|
||||
|
||||
return Element.wrap_dom_element(self._dom_element.parentElement)
|
||||
@@ -1124,7 +1136,7 @@ class video(ContainerElement):
|
||||
width = width if width is not None else self.videoWidth
|
||||
height = height if height is not None else self.videoHeight
|
||||
|
||||
if to is None:
|
||||
if is_none(to):
|
||||
to = canvas(width=width, height=height)
|
||||
|
||||
elif isinstance(to, Element):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import js
|
||||
from pyscript.ffi import create_proxy
|
||||
from pyscript.util import as_bytearray
|
||||
from pyscript.util import as_bytearray, is_awaitable
|
||||
|
||||
code = "code"
|
||||
protocols = "protocols"
|
||||
@@ -8,6 +8,23 @@ reason = "reason"
|
||||
methods = ["onclose", "onerror", "onmessage", "onopen"]
|
||||
|
||||
|
||||
def add_listener(socket, onevent, listener):
|
||||
p = create_proxy(listener)
|
||||
|
||||
if is_awaitable(listener):
|
||||
|
||||
async def wrapper(e):
|
||||
await p(EventMessage(e))
|
||||
|
||||
m = wrapper
|
||||
|
||||
else:
|
||||
m = lambda e: p(EventMessage(e))
|
||||
|
||||
# Pyodide fails at setting socket[onevent] directly
|
||||
setattr(socket, onevent, m)
|
||||
|
||||
|
||||
class EventMessage:
|
||||
def __init__(self, event):
|
||||
self._event = event
|
||||
@@ -36,20 +53,20 @@ class WebSocket:
|
||||
socket = js.WebSocket.new(url, kw[protocols])
|
||||
else:
|
||||
socket = js.WebSocket.new(url)
|
||||
|
||||
socket.binaryType = "arraybuffer"
|
||||
object.__setattr__(self, "_ws", socket)
|
||||
|
||||
for t in methods:
|
||||
if t in kw:
|
||||
# Pyodide fails at setting socket[t] directly
|
||||
setattr(socket, t, create_proxy(kw[t]))
|
||||
add_listener(socket, t, kw[t])
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self._ws, attr)
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
if attr in methods:
|
||||
m = lambda e: value(EventMessage(e))
|
||||
setattr(self._ws, attr, create_proxy(m))
|
||||
add_listener(self._ws, attr, value)
|
||||
else:
|
||||
setattr(self._ws, attr, value)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export default {
|
||||
* Ask a user action via dialog and returns the directory handler once granted.
|
||||
* @param {string} uid
|
||||
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
|
||||
* @returns {boolean}
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async storeFSHandler(uid, options = {}) {
|
||||
if (await idb.has(uid)) return true;
|
||||
@@ -28,4 +28,15 @@ export default {
|
||||
() => false,
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Explicitly remove the unique identifier for the FS handler.
|
||||
* @param {string} uid
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async deleteFSHandler(uid) {
|
||||
const had = await idb.has(uid);
|
||||
if (had) await idb.delete(uid);
|
||||
return had;
|
||||
},
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
1
core/tests/javascript/config-parser/file.py
Normal file
1
core/tests/javascript/config-parser/file.py
Normal file
@@ -0,0 +1 @@
|
||||
print("OK")
|
||||
21
core/tests/javascript/config-parser/index.html
Normal file
21
core/tests/javascript/config-parser/index.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="../../../dist/core.css">
|
||||
<script type="module" src="../../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<py-config config-parser="https://esm.run/basic-toml">
|
||||
[files]
|
||||
'file.py' = ""
|
||||
</py-config>
|
||||
<script type="py">
|
||||
import file
|
||||
from pyscript import document
|
||||
document.documentElement.classList.add("done")
|
||||
document.body.append("OK")
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,7 +2,7 @@ from pyscript import document
|
||||
|
||||
classList = document.documentElement.classList
|
||||
|
||||
if not __terminal__:
|
||||
if not __terminal__: # noqa: F821 __terminal__ is defined in core/src/plugins/donkey.js
|
||||
classList.add("error")
|
||||
else:
|
||||
classList.add("ok")
|
||||
|
||||
36
core/tests/javascript/worker-symbols.html
Normal file
36
core/tests/javascript/worker-symbols.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PyScript VS Symbols</title>
|
||||
<script>
|
||||
globalThis.hasSymbol = (symbol, ref) => symbol in ref;
|
||||
globalThis.getSymbol = (symbol, ref) => ref[symbol];
|
||||
|
||||
// some 3rd party JS library might use symbols to brand-check
|
||||
// so it's not about symbols traveling from MicroPython
|
||||
// it's about MicroPython proxies traps not understanding symbols
|
||||
globalThis.hasIterator = ref => Symbol.iterator in ref;
|
||||
</script>
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
<script type="mpy">
|
||||
import js
|
||||
|
||||
symbol = js.Symbol.iterator
|
||||
|
||||
if js.getSymbol(symbol, []) and js.hasSymbol(symbol, []) and js.hasIterator([]):
|
||||
js.document.documentElement.classList.add("main")
|
||||
</script>
|
||||
<script type="mpy" worker>
|
||||
from pyscript import window
|
||||
import js
|
||||
|
||||
symbol = js.Symbol.iterator
|
||||
|
||||
if window.getSymbol(symbol, []) and window.hasSymbol(symbol, []) and window.hasIterator([]):
|
||||
window.document.documentElement.classList.add("worker")
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
||||
@@ -14,7 +14,7 @@
|
||||
print(event.type)
|
||||
ws.send("hello")
|
||||
|
||||
def onmessage(event):
|
||||
async def onmessage(event):
|
||||
print(event.type, event.data)
|
||||
ws.close()
|
||||
|
||||
|
||||
@@ -2,6 +2,13 @@ import { test, expect } from '@playwright/test';
|
||||
|
||||
test.setTimeout(120 * 1000);
|
||||
|
||||
test('config-parser custom TOML', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/javascript/config-parser/index.html');
|
||||
await page.waitForSelector('html.done');
|
||||
const body = await page.evaluate(() => document.body.innerText);
|
||||
await expect(body.trim()).toBe('OK');
|
||||
});
|
||||
|
||||
test('MicroPython display', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/javascript/mpy.html');
|
||||
await page.waitForSelector('html.done.worker');
|
||||
@@ -59,6 +66,11 @@ test('MicroPython + configURL', async ({ page }) => {
|
||||
await page.waitForSelector('html.main.worker');
|
||||
});
|
||||
|
||||
test('MicroPython + Symbols', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/javascript/worker-symbols.html');
|
||||
await page.waitForSelector('html.main.worker');
|
||||
});
|
||||
|
||||
test('Pyodide + terminal on Main', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/javascript/py-terminal-main.html');
|
||||
await page.waitForSelector('html.ok');
|
||||
|
||||
21
core/tests/manual/ffi_timeout/index.html
Normal file
21
core/tests/manual/ffi_timeout/index.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<link rel="stylesheet" href="../../../dist/core.css">
|
||||
<script>
|
||||
window.Worker = class extends Worker {
|
||||
constructor(url, ...rest) {
|
||||
console.log(rest[0]);
|
||||
return super(url, ...rest);
|
||||
}
|
||||
};
|
||||
window.start = Date.now();
|
||||
</script>
|
||||
<script type="module" src="../../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="py" config="./index.toml" src="index.py" worker></script>
|
||||
</body>
|
||||
</html>
|
||||
3
core/tests/manual/ffi_timeout/index.py
Normal file
3
core/tests/manual/ffi_timeout/index.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from pyscript import document, window
|
||||
|
||||
document.body.append(window.Date.now() - window.start)
|
||||
2
core/tests/manual/ffi_timeout/index.toml
Normal file
2
core/tests/manual/ffi_timeout/index.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
experimental_ffi_timeout = 0
|
||||
package_cache = "passthrough"
|
||||
@@ -19,6 +19,8 @@ if TEST == "implicit":
|
||||
|
||||
await fs.sync("/persistent")
|
||||
|
||||
# await fs.revoke("/persistent")
|
||||
|
||||
elif not RUNNING_IN_WORKER:
|
||||
from pyscript import document
|
||||
|
||||
@@ -39,7 +41,7 @@ elif not RUNNING_IN_WORKER:
|
||||
js.alert("unable to grant access")
|
||||
|
||||
async def unmount(event):
|
||||
await fs.unmount("/persistent")
|
||||
await fs.revoke("/persistent")
|
||||
button.textContent = "mount"
|
||||
button.onclick = mount
|
||||
|
||||
|
||||
@@ -318,16 +318,12 @@ async def main(winstyle=0):
|
||||
if not fullscreen:
|
||||
print("Changing to FULLSCREEN")
|
||||
screen_backup = screen.copy()
|
||||
screen = pygame.display.set_mode(
|
||||
SCREENRECT.size, winstyle | pygame.FULLSCREEN, bestdepth
|
||||
)
|
||||
screen = pygame.display.set_mode(SCREENRECT.size, winstyle | pygame.FULLSCREEN)
|
||||
screen.blit(screen_backup, (0, 0))
|
||||
else:
|
||||
print("Changing to windowed mode")
|
||||
screen_backup = screen.copy()
|
||||
screen = pygame.display.set_mode(
|
||||
SCREENRECT.size, winstyle, bestdepth
|
||||
)
|
||||
screen = pygame.display.set_mode(SCREENRECT.size, winstyle)
|
||||
screen.blit(screen_backup, (0, 0))
|
||||
pygame.display.flip()
|
||||
fullscreen = not fullscreen
|
||||
|
||||
@@ -140,7 +140,7 @@ def get_stats_gl(renderer):
|
||||
|
||||
def bg_from_v(*vertices):
|
||||
geometry = new(THREE.BufferGeometry)
|
||||
vertices_f32a = new(Float32Array, vertices)
|
||||
vertices_f32a = new(Float32Array, vertices) # noqa: F821 Float32Array is defined in js
|
||||
attr = new(THREE.Float32BufferAttribute, vertices_f32a, 3)
|
||||
return geometry.setAttribute('position', attr)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from pyscript import document, window, PyWorker
|
||||
from libthree import THREE, clear, SoundPlayer
|
||||
from libthree import get_renderer, get_ortho_camera
|
||||
from libthree import get_loading_manager, get_stats_gl
|
||||
from libthree import lsgeo, line2, linemat, lsgeo
|
||||
from libthree import line2, linemat, lsgeo
|
||||
from libfft import BeatSync
|
||||
|
||||
from multipyjs import MICROPYTHON, new, call, to_js, create_proxy
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
<link rel="stylesheet" href="../../../dist/core.css">
|
||||
<script type="module" src="../../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="py-editor">
|
||||
|
||||
34
core/tests/manual/remote/index.html
Normal file
34
core/tests/manual/remote/index.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>PyScript Custom Package</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
const track = ({ detail }) => {
|
||||
if (/^(Loading|Loaded)/.test(detail)) {
|
||||
const state = RegExp.$1;
|
||||
const what = detail.slice(state.length + 1);
|
||||
if (state === 'Loading') console.time(what);
|
||||
else console.timeEnd(what);
|
||||
}
|
||||
};
|
||||
addEventListener("py:progress", track);
|
||||
addEventListener("mpy:progress", track);
|
||||
</script>
|
||||
<!-- PyScript (locally) -->
|
||||
<link rel="stylesheet" href="../../../dist/core.css">
|
||||
<script type="module" src="../../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div>loading ...</div>
|
||||
<script type="py" config="./package.toml">
|
||||
import custom_package
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
3
core/tests/manual/remote/package.toml
Normal file
3
core/tests/manual/remote/package.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
experimental_remote_packages = true
|
||||
|
||||
packages = ['https://webreflection.github.io/examples/pyscript/module/package.toml']
|
||||
@@ -24,5 +24,6 @@
|
||||
"./example_js_worker_module.js": "greeting_worker"
|
||||
}
|
||||
},
|
||||
"packages": ["Pillow" ]
|
||||
"packages": ["Pillow" ],
|
||||
"experimental_ffi_timeout": 0
|
||||
}
|
||||
|
||||
@@ -358,3 +358,79 @@ def test_when_called_with_an_event_and_handler():
|
||||
# The function should have been called when the whenable object was
|
||||
# triggered.
|
||||
assert counter == 1
|
||||
|
||||
def test_when_on_different_callables():
|
||||
"""
|
||||
The when function works with:
|
||||
|
||||
* Synchronous functions
|
||||
* Asynchronous functions
|
||||
* Inner functions
|
||||
* Async inner functions
|
||||
* Closure functions
|
||||
* Async closure functions
|
||||
"""
|
||||
|
||||
def func(x=1):
|
||||
# A simple function.
|
||||
return x
|
||||
|
||||
async def a_func(x=1):
|
||||
# A simple async function.
|
||||
return x
|
||||
|
||||
def make_inner_func():
|
||||
# Creates a simple inner function.
|
||||
|
||||
def inner_func(x=1):
|
||||
return x
|
||||
|
||||
return inner_func
|
||||
|
||||
|
||||
def make_inner_a_func():
|
||||
# Creates a simple async inner function.
|
||||
|
||||
async def inner_a_func(x=1):
|
||||
return x
|
||||
|
||||
return inner_a_func
|
||||
|
||||
|
||||
def make_closure():
|
||||
# Creates a simple closure function.
|
||||
a = 1
|
||||
|
||||
def closure_func(x=1):
|
||||
return a + x
|
||||
|
||||
return closure_func
|
||||
|
||||
|
||||
def make_a_closure():
|
||||
# Creates a simple async closure function.
|
||||
a = 1
|
||||
|
||||
async def closure_a_func(x=1):
|
||||
return a + x
|
||||
|
||||
return closure_a_func
|
||||
|
||||
|
||||
inner_func = make_inner_func()
|
||||
inner_a_func = make_inner_a_func()
|
||||
cl_func = make_closure()
|
||||
cl_a_func = make_a_closure()
|
||||
|
||||
|
||||
whenable = Event()
|
||||
|
||||
# Each of these should work with the when function.
|
||||
when(whenable, func)
|
||||
when(whenable, a_func)
|
||||
when(whenable, inner_func)
|
||||
when(whenable, inner_a_func)
|
||||
when(whenable, cl_func)
|
||||
when(whenable, cl_a_func)
|
||||
# If we get here, everything worked.
|
||||
assert True
|
||||
|
||||
@@ -32,7 +32,7 @@ def test_js_module_is_available_on_worker():
|
||||
|
||||
|
||||
@upytest.skip("Worker only.", skip_when=not RUNNING_IN_WORKER)
|
||||
def test_js_module_is_available_on_worker():
|
||||
def test_js_module_is_available_on_greeting_worker():
|
||||
"""
|
||||
The "hello" function in the example_js_worker_module.js file is available
|
||||
via the js_modules object while running in a worker.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
Tests for the PyScript media module.
|
||||
"""
|
||||
|
||||
from pyscript import media
|
||||
import upytest
|
||||
|
||||
from pyscript import media
|
||||
|
||||
@@ -11,7 +11,7 @@ def test_as_bytearray():
|
||||
msg = b"Hello, world!"
|
||||
buffer = js.ArrayBuffer.new(len(msg))
|
||||
ui8a = js.Uint8Array.new(buffer)
|
||||
for b in msg:
|
||||
for i, b in enumerate(msg):
|
||||
ui8a[i] = b
|
||||
ba = util.as_bytearray(buffer)
|
||||
assert isinstance(ba, bytearray)
|
||||
|
||||
@@ -871,7 +871,17 @@ class TestElements:
|
||||
self._create_el_and_basic_asserts("kbd", "some text")
|
||||
|
||||
def test_label(self):
|
||||
self._create_el_and_basic_asserts("label", "some text")
|
||||
label_text = "Luke, I am your father"
|
||||
label_for = "some-id"
|
||||
# Let's create the element
|
||||
el = web.label(label_text, for_=label_for)
|
||||
# Let's check the element was configured correctly.
|
||||
assert isinstance(el, web.label), "The new element should be a label."
|
||||
assert el.textContent == label_text, "The label text should match."
|
||||
assert el._dom_element.tagName == "LABEL"
|
||||
assert el.for_ == label_for, "The label should have the correct for attribute."
|
||||
# Ensure the label element is rendered with the correct "for" attribute
|
||||
assert f'for="{label_for}"' in el.outerHTML, "The label should have the correct 'for' attribute in its HTML."
|
||||
|
||||
def test_legend(self):
|
||||
self._create_el_and_basic_asserts("legend", "some text")
|
||||
|
||||
2
core/types/core.d.ts
vendored
2
core/types/core.d.ts
vendored
@@ -63,5 +63,5 @@ declare const exportedHooks: {
|
||||
};
|
||||
};
|
||||
declare const exportedConfig: {};
|
||||
declare const exportedWhenDefined: any;
|
||||
declare const exportedWhenDefined: (type: string) => Promise<object>;
|
||||
export { codemirror, stdlib, optional, inputFailure, TYPES, relative_url, exportedPyWorker as PyWorker, exportedMPWorker as MPWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined };
|
||||
|
||||
10
core/types/sync.d.ts
vendored
10
core/types/sync.d.ts
vendored
@@ -9,12 +9,18 @@ declare namespace _default {
|
||||
* Ask a user action via dialog and returns the directory handler once granted.
|
||||
* @param {string} uid
|
||||
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
|
||||
* @returns {boolean}
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
function storeFSHandler(uid: string, options?: {
|
||||
id?: string;
|
||||
mode?: "read" | "readwrite";
|
||||
hint?: "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
|
||||
}): boolean;
|
||||
}): Promise<boolean>;
|
||||
/**
|
||||
* Explicitly remove the unique identifier for the FS handler.
|
||||
* @param {string} uid
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
function deleteFSHandler(uid: string): Promise<boolean>;
|
||||
}
|
||||
export default _default;
|
||||
|
||||
@@ -137,12 +137,14 @@
|
||||
<body>
|
||||
<h1>Hello world!</h1>
|
||||
<p>These are the Python interpreters in PyScript _VERSION_:</p>
|
||||
<script type="py"> <!-- Pyodide -->
|
||||
<script type="py">
|
||||
# Pyodide
|
||||
from pyscript import display
|
||||
import sys
|
||||
display(sys.version)
|
||||
</script>
|
||||
<script type="mpy"> <!-- MicroPython -->
|
||||
<script type="mpy">
|
||||
# MicroPython
|
||||
from pyscript import display
|
||||
import sys
|
||||
display(sys.version)
|
||||
|
||||
@@ -5,5 +5,5 @@ skip = "*.js,*.json"
|
||||
[tool.ruff]
|
||||
line-length = 114
|
||||
lint.select = ["C4", "C90", "E", "EM", "F", "PIE", "PYI", "PLC", "Q", "RET", "W"]
|
||||
lint.ignore = ["E402", "E722", "E731", "E741", "F401", "F704", "F811", "F821"]
|
||||
lint.ignore = ["E402", "E722", "E731", "E741", "F401", "F704", "PLC0415"]
|
||||
lint.mccabe.max-complexity = 27
|
||||
|
||||
Reference in New Issue
Block a user