mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-21 11:15:36 -05:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8250f2c8c | ||
|
|
83b41f9928 | ||
|
|
a8684a2168 | ||
|
|
f8cf58d6c4 | ||
|
|
8cd9c4c382 | ||
|
|
1f609233e7 | ||
|
|
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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
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
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
run: tar -cvf ../release.tar * && mv ../release.tar .
|
run: tar -cvf ../release.tar * && mv ../release.tar .
|
||||||
|
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v4
|
uses: aws-actions/configure-aws-credentials@v5
|
||||||
with:
|
with:
|
||||||
aws-region: ${{ secrets.AWS_REGION }}
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
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
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v4
|
uses: aws-actions/configure-aws-credentials@v5
|
||||||
with:
|
with:
|
||||||
aws-region: ${{ secrets.AWS_REGION }}
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
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
|
working-directory: ./core
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
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
|
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
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v4
|
uses: aws-actions/configure-aws-credentials@v5
|
||||||
with:
|
with:
|
||||||
aws-region: ${{ secrets.AWS_REGION }}
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
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
|
MINICONDA_VERSION: 4.11.0
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 3
|
fetch-depth: 3
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
run: git log --graph -3
|
run: git log --graph -3
|
||||||
|
|
||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ ci:
|
|||||||
default_stages: [pre-commit]
|
default_stages: [pre-commit]
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v5.0.0
|
rev: v6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-builtin-literals
|
- id: check-builtin-literals
|
||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
@@ -24,8 +24,8 @@ repos:
|
|||||||
exclude: core/dist|\.min\.js$
|
exclude: core/dist|\.min\.js$
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
|
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||||
rev: 25.1.0
|
rev: 25.9.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
exclude: core/tests
|
exclude: core/tests
|
||||||
@@ -40,7 +40,7 @@ repos:
|
|||||||
- tomli
|
- tomli
|
||||||
|
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.11.8
|
rev: v0.13.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
exclude: core/tests
|
exclude: core/tests
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
ISSUE_TEMPLATE
|
ISSUE_TEMPLATE
|
||||||
*.min.*
|
*.min.*
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
bridge/
|
||||||
|
|||||||
@@ -13,15 +13,15 @@ Using PyScript is as simple as:
|
|||||||
<title>PyScript!</title>
|
<title>PyScript!</title>
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://pyscript.net/snapshots/2024.9.2/core.css"
|
href="https://pyscript.net/releases/2025.11.1/core.css"
|
||||||
/>
|
/>
|
||||||
<script
|
<script
|
||||||
type="module"
|
type="module"
|
||||||
src="https://pyscript.net/snapshots/2024.9.2/core.js"
|
src="https://pyscript.net/releases/2025.11.1/core.js"
|
||||||
></script>
|
></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Use MicroPython to evaluate some Python -->
|
<!-- type mpy (MicroPython) or py (Pyodide) to run some Python -->
|
||||||
<script type="mpy" terminal>
|
<script type="mpy" terminal>
|
||||||
print("Hello, world!")
|
print("Hello, world!")
|
||||||
</script>
|
</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",
|
"name": "@pyscript/core",
|
||||||
"version": "0.6.53",
|
"version": "0.7.11",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "PyScript",
|
"description": "PyScript",
|
||||||
"module": "./index.js",
|
"module": "./index.js",
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"server": "echo \"➡️ TESTS @ $(tput bold)http://localhost:8080/tests/$(tput sgr0)\"; npx static-handler --coi .",
|
"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:core": "rm -rf dist && rollup --config rollup/core.config.js && cp src/3rd-party/*.css dist/",
|
||||||
"build:flatted": "node rollup/flatted.cjs",
|
"build:flatted": "node rollup/flatted.cjs",
|
||||||
"build:plugins": "node rollup/plugins.cjs",
|
"build:plugins": "node rollup/plugins.cjs",
|
||||||
@@ -67,40 +67,40 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ungap/with-resolvers": "^0.1.0",
|
"@ungap/with-resolvers": "^0.1.0",
|
||||||
"@webreflection/idb-map": "^0.3.2",
|
"@webreflection/idb-map": "^0.3.2",
|
||||||
"@webreflection/utils": "^0.1.0",
|
"@webreflection/utils": "^0.1.1",
|
||||||
"add-promise-listener": "^0.1.3",
|
"add-promise-listener": "^0.1.3",
|
||||||
"basic-devtools": "^0.1.6",
|
"basic-devtools": "^0.1.6",
|
||||||
"polyscript": "^0.17.20",
|
"polyscript": "^0.19.10",
|
||||||
"sticky-module": "^0.1.1",
|
"sticky-module": "^0.1.1",
|
||||||
"to-json-callback": "^0.1.1",
|
"to-json-callback": "^0.1.1",
|
||||||
"type-checked-collections": "^0.1.7"
|
"type-checked-collections": "^0.1.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@codemirror/commands": "^6.8.1",
|
"@codemirror/commands": "^6.10.0",
|
||||||
"@codemirror/lang-python": "^6.2.1",
|
"@codemirror/lang-python": "^6.2.1",
|
||||||
"@codemirror/language": "^6.11.0",
|
"@codemirror/language": "^6.11.3",
|
||||||
"@codemirror/state": "^6.5.2",
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/view": "^6.36.8",
|
"@codemirror/view": "^6.38.6",
|
||||||
"@playwright/test": "^1.52.0",
|
"@playwright/test": "^1.56.1",
|
||||||
"@rollup/plugin-commonjs": "^28.0.3",
|
"@rollup/plugin-commonjs": "^29.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||||
"@rollup/plugin-terser": "^0.4.4",
|
"@rollup/plugin-terser": "^0.4.4",
|
||||||
"@webreflection/toml-j0.4": "^1.1.4",
|
"@webreflection/toml-j0.4": "^1.1.4",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/addon-web-links": "^0.11.0",
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"bun": "^1.2.13",
|
"bun": "^1.3.1",
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
"codedent": "^0.1.2",
|
"codedent": "^0.1.2",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.2",
|
||||||
"eslint": "^9.27.0",
|
"eslint": "^9.39.1",
|
||||||
"flatted": "^3.3.3",
|
"flatted": "^3.3.3",
|
||||||
"rollup": "^4.41.0",
|
"rollup": "^4.52.5",
|
||||||
"rollup-plugin-postcss": "^4.0.2",
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
"rollup-plugin-string": "^3.0.0",
|
"rollup-plugin-string": "^3.0.0",
|
||||||
"static-handler": "^0.5.3",
|
"static-handler": "^0.5.3",
|
||||||
"string-width": "^7.2.0",
|
"string-width": "^8.1.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.9.3",
|
||||||
"xterm-readline": "^1.1.2"
|
"xterm-readline": "^1.1.2"
|
||||||
},
|
},
|
||||||
"repository": {
|
"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
|
* 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
|
* 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,
|
let config,
|
||||||
type,
|
type,
|
||||||
|
parser,
|
||||||
pyElement,
|
pyElement,
|
||||||
pyConfigs = $$(`${TYPE}-config`),
|
pyConfigs = $$(`${TYPE}-config`),
|
||||||
attrConfigs = $$(
|
attrConfigs = $$(
|
||||||
@@ -92,9 +93,11 @@ for (const [TYPE] of TYPES) {
|
|||||||
[pyElement] = pyConfigs;
|
[pyElement] = pyConfigs;
|
||||||
config = pyElement.getAttribute("src") || pyElement.textContent;
|
config = pyElement.getAttribute("src") || pyElement.textContent;
|
||||||
type = pyElement.getAttribute("type");
|
type = pyElement.getAttribute("type");
|
||||||
|
parser = pyElement.getAttribute("config-parser");
|
||||||
} else if (attrConfigs.length) {
|
} else if (attrConfigs.length) {
|
||||||
[pyElement, ...attrConfigs] = attrConfigs;
|
[pyElement, ...attrConfigs] = attrConfigs;
|
||||||
config = pyElement.getAttribute("config");
|
config = pyElement.getAttribute("config");
|
||||||
|
parser = pyElement.getAttribute("config-parser");
|
||||||
// throw an error if dirrent scripts use different configs
|
// throw an error if dirrent scripts use different configs
|
||||||
if (
|
if (
|
||||||
attrConfigs.some((el) => el.getAttribute("config") !== config)
|
attrConfigs.some((el) => el.getAttribute("config") !== config)
|
||||||
@@ -120,9 +123,12 @@ for (const [TYPE] of TYPES) {
|
|||||||
}
|
}
|
||||||
} else if (toml || type === "toml") {
|
} else if (toml || type === "toml") {
|
||||||
try {
|
try {
|
||||||
const { parse } = await import(
|
const module = parser
|
||||||
/* webpackIgnore: true */ "./3rd-party/toml.js"
|
? await import(parser)
|
||||||
);
|
: await import(
|
||||||
|
/* webpackIgnore: true */ "./3rd-party/toml.js"
|
||||||
|
);
|
||||||
|
const parse = module.parse || module.default;
|
||||||
parsed = parse(text);
|
parsed = parse(text);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = syntaxError("TOML", url, e);
|
error = syntaxError("TOML", url, e);
|
||||||
@@ -154,6 +160,9 @@ for (const [TYPE] of TYPES) {
|
|||||||
return await Promise.all(toBeAwaited);
|
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 });
|
configs.set(TYPE, { config: parsed, configURL, plugins, error });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const getRelatedScript = (target, type) => {
|
|||||||
return editor?.parentNode?.previousElementSibling;
|
return editor?.parentNode?.previousElementSibling;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function execute({ currentTarget }) {
|
async function execute({ currentTarget, script }) {
|
||||||
const { env, pySrc, outDiv } = this;
|
const { env, pySrc, outDiv } = this;
|
||||||
const hasRunButton = !!currentTarget;
|
const hasRunButton = !!currentTarget;
|
||||||
|
|
||||||
@@ -91,14 +91,13 @@ async function execute({ currentTarget }) {
|
|||||||
// creation and destruction of editors on the fly
|
// creation and destruction of editors on the fly
|
||||||
if (hasRunButton) {
|
if (hasRunButton) {
|
||||||
for (const type of TYPES.keys()) {
|
for (const type of TYPES.keys()) {
|
||||||
const script = getRelatedScript(currentTarget, type);
|
script = getRelatedScript(currentTarget, type);
|
||||||
if (script) {
|
if (script) break;
|
||||||
defineProperties(script, { xworker: { value: xworker } });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineProperties(script, { xworker: { value: xworker } });
|
||||||
|
|
||||||
const { sync } = xworker;
|
const { sync } = xworker;
|
||||||
const { promise, resolve } = withResolvers();
|
const { promise, resolve } = withResolvers();
|
||||||
envs.set(env, promise);
|
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 makeRunButton = (handler, type) => {
|
||||||
const runButton = document.createElement("button");
|
const runButton = document.createElement("button");
|
||||||
runButton.className = `absolute ${type}-editor-run-button`;
|
runButton.className = `absolute ${type}-editor-run-button`;
|
||||||
@@ -169,15 +182,25 @@ const makeRunButton = (handler, type) => {
|
|||||||
) {
|
) {
|
||||||
const script = getRelatedScript(runButton, type);
|
const script = getRelatedScript(runButton, type);
|
||||||
if (script) {
|
if (script) {
|
||||||
const editor = editors.get(script);
|
const env = script.getAttribute("env");
|
||||||
const content = editor.state.doc.toString();
|
// remove the bootstrapped env which could be one or shared
|
||||||
const clone = script.cloneNode(true);
|
if (env) {
|
||||||
clone.type = `${type}-editor`;
|
for (const [key, value] of TYPES) {
|
||||||
clone.textContent = content;
|
if (key === type) {
|
||||||
script.xworker.terminate();
|
configs.delete(`${value}-${env}`);
|
||||||
script.nextElementSibling.remove();
|
envs.delete(`${value}-${env}`);
|
||||||
script.replaceWith(clone);
|
break;
|
||||||
editors.delete(script);
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -365,7 +388,7 @@ const init = async (script, type, interpreter) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (isSetup) {
|
if (isSetup) {
|
||||||
await context.handleEvent({ currentTarget: null });
|
await context.handleEvent({ currentTarget: null, script });
|
||||||
notifyEditor();
|
notifyEditor();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -403,16 +426,17 @@ const init = async (script, type, interpreter) => {
|
|||||||
// preserve user indentation, if any
|
// preserve user indentation, if any
|
||||||
const indentation = /^([ \t]+)/m.test(doc) ? RegExp.$1 : " ";
|
const indentation = /^([ \t]+)/m.test(doc) ? RegExp.$1 : " ";
|
||||||
|
|
||||||
const listener = () => runButton.click();
|
const listener = () => !runButton.click();
|
||||||
const editor = new EditorView({
|
const editor = new EditorView({
|
||||||
extensions: [
|
extensions: [
|
||||||
indentUnit.of(indentation),
|
indentUnit.of(indentation),
|
||||||
new Compartment().of(python()),
|
new Compartment().of(python()),
|
||||||
keymap.of([
|
keymap.of([
|
||||||
...defaultKeymap,
|
|
||||||
{ key: "Ctrl-Enter", run: listener, preventDefault: true },
|
{ key: "Ctrl-Enter", run: listener, preventDefault: true },
|
||||||
{ key: "Cmd-Enter", run: listener, preventDefault: true },
|
{ key: "Cmd-Enter", run: listener, preventDefault: true },
|
||||||
{ key: "Shift-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/
|
// @see https://codemirror.net/examples/tab/
|
||||||
indentWithTab,
|
indentWithTab,
|
||||||
]),
|
]),
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -4,6 +4,7 @@ import io
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from pyscript.magic_js import current_target, document, window
|
from pyscript.magic_js import current_target, document, window
|
||||||
|
from pyscript.ffi import is_none
|
||||||
|
|
||||||
_MIME_METHODS = {
|
_MIME_METHODS = {
|
||||||
"savefig": "image/png",
|
"savefig": "image/png",
|
||||||
@@ -105,13 +106,13 @@ def _format_mime(obj):
|
|||||||
else:
|
else:
|
||||||
output = _eval_formatter(obj, method)
|
output = _eval_formatter(obj, method)
|
||||||
|
|
||||||
if output is None:
|
if is_none(output):
|
||||||
continue
|
continue
|
||||||
if mime_type not in _MIME_RENDERERS:
|
if mime_type not in _MIME_RENDERERS:
|
||||||
not_available.append(mime_type)
|
not_available.append(mime_type)
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
if output is None:
|
if is_none(output):
|
||||||
if not_available:
|
if not_available:
|
||||||
window.console.warn(
|
window.console.warn(
|
||||||
f"Rendered object requested unavailable MIME renderers: {not_available}"
|
f"Rendered object requested unavailable MIME renderers: {not_available}"
|
||||||
@@ -135,7 +136,7 @@ def _write(element, value, append=False):
|
|||||||
element.append(out_element)
|
element.append(out_element)
|
||||||
else:
|
else:
|
||||||
out_element = element.lastElementChild
|
out_element = element.lastElementChild
|
||||||
if out_element is None:
|
if is_none(out_element):
|
||||||
out_element = element
|
out_element = element
|
||||||
|
|
||||||
if mime_type in ("application/javascript", "text/html"):
|
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):
|
def display(*values, target=None, append=True):
|
||||||
if target is None:
|
if is_none(target):
|
||||||
target = current_target()
|
target = current_target()
|
||||||
elif not isinstance(target, str):
|
elif not isinstance(target, str):
|
||||||
msg = f"target must be str or None, not {target.__class__.__name__}"
|
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)
|
element = document.getElementById(target)
|
||||||
|
|
||||||
# If target cannot be found on the page, a ValueError is raised
|
# 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."
|
msg = f"Invalid selector with id={target}. Cannot be found in the page."
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
|||||||
@@ -92,60 +92,26 @@ def when(target, *args, **kwargs):
|
|||||||
elements = selector if isinstance(selector, list) else [selector]
|
elements = selector if isinstance(selector, list) else [selector]
|
||||||
|
|
||||||
def decorator(func):
|
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):
|
if is_awaitable(func):
|
||||||
|
|
||||||
async def wrapper(*args, **kwargs):
|
async def wrapper(*args, **kwargs):
|
||||||
"""
|
return await func()
|
||||||
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
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
"""
|
return func()
|
||||||
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()
|
|
||||||
|
|
||||||
wrapper = wraps(func)(wrapper)
|
wrapper = wraps(func)(wrapper)
|
||||||
if isinstance(target, Event):
|
if isinstance(target, Event):
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ try:
|
|||||||
import js
|
import js
|
||||||
from pyodide.ffi import create_proxy as _cp
|
from pyodide.ffi import create_proxy as _cp
|
||||||
from pyodide.ffi import to_js as _py_tjs
|
from pyodide.ffi import to_js as _py_tjs
|
||||||
|
from pyodide.ffi import jsnull
|
||||||
|
|
||||||
from_entries = js.Object.fromEntries
|
from_entries = js.Object.fromEntries
|
||||||
|
is_none = lambda value: value is None or value is jsnull
|
||||||
|
|
||||||
def _tjs(value, **kw):
|
def _tjs(value, **kw):
|
||||||
if not hasattr(kw, "dict_converter"):
|
if not hasattr(kw, "dict_converter"):
|
||||||
@@ -13,6 +15,34 @@ try:
|
|||||||
except:
|
except:
|
||||||
from jsffi import create_proxy as _cp
|
from jsffi import create_proxy as _cp
|
||||||
from jsffi import to_js as _tjs
|
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
|
create_proxy = _cp
|
||||||
to_js = _tjs
|
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
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
mounted = {}
|
mounted = {}
|
||||||
|
|
||||||
|
|
||||||
|
async def get_handler(details):
|
||||||
|
handler = details.handler
|
||||||
|
options = details.options
|
||||||
|
permission = await handler.queryPermission(options)
|
||||||
|
return handler if permission == "granted" else None
|
||||||
|
|
||||||
|
|
||||||
async def mount(path, mode="readwrite", root="", id="pyscript"):
|
async def mount(path, mode="readwrite", root="", id="pyscript"):
|
||||||
import js
|
import js
|
||||||
from _pyscript import fs, interpreter
|
from _pyscript import fs, interpreter
|
||||||
@@ -12,6 +19,7 @@ async def mount(path, mode="readwrite", root="", id="pyscript"):
|
|||||||
|
|
||||||
js.console.warn("experimental pyscript.fs ⚠️")
|
js.console.warn("experimental pyscript.fs ⚠️")
|
||||||
|
|
||||||
|
details = None
|
||||||
handler = None
|
handler = None
|
||||||
|
|
||||||
uid = f"{path}@{id}"
|
uid = f"{path}@{id}"
|
||||||
@@ -31,9 +39,16 @@ async def mount(path, mode="readwrite", root="", id="pyscript"):
|
|||||||
|
|
||||||
if success:
|
if success:
|
||||||
from polyscript import IDBMap
|
from polyscript import IDBMap
|
||||||
|
from pyscript import window
|
||||||
|
|
||||||
|
idbm = IDBMap.new(fs.NAMESPACE)
|
||||||
|
details = await idbm.get(uid)
|
||||||
|
handler = await get_handler(details)
|
||||||
|
if handler is None:
|
||||||
|
# force await in either async or sync scenario
|
||||||
|
await js.Promise.resolve(sync.getFSHandler(details.options))
|
||||||
|
handler = details.handler
|
||||||
|
|
||||||
idb = IDBMap.new(fs.NAMESPACE)
|
|
||||||
handler = await idb.get(uid)
|
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(fs.ERROR)
|
raise RuntimeError(fs.ERROR)
|
||||||
|
|
||||||
@@ -41,14 +56,41 @@ async def mount(path, mode="readwrite", root="", id="pyscript"):
|
|||||||
success = await fs.idb.has(uid)
|
success = await fs.idb.has(uid)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
handler = await fs.idb.get(uid)
|
details = await fs.idb.get(uid)
|
||||||
|
handler = await get_handler(details)
|
||||||
|
if handler is None:
|
||||||
|
handler = await fs.getFileSystemDirectoryHandle(details.options)
|
||||||
else:
|
else:
|
||||||
handler = await fs.getFileSystemDirectoryHandle(to_js(options))
|
js_options = to_js(options)
|
||||||
await fs.idb.set(uid, handler)
|
handler = await fs.getFileSystemDirectoryHandle(js_options)
|
||||||
|
details = {"handler": handler, "options": js_options}
|
||||||
|
await fs.idb.set(uid, to_js(details))
|
||||||
|
|
||||||
mounted[path] = await interpreter.mountNativeFS(path, handler)
|
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):
|
async def sync(path):
|
||||||
await mounted[path].syncfs()
|
await mounted[path].syncfs()
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,11 @@ if RUNNING_IN_WORKER:
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
import _pyscript
|
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
|
window = globalThis
|
||||||
document = globalThis.document
|
document = globalThis.document
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
from polyscript import storage as _storage
|
from polyscript import storage as _storage
|
||||||
from pyscript.flatted import parse as _parse
|
from pyscript.flatted import parse as _parse
|
||||||
from pyscript.flatted import stringify as _stringify
|
from pyscript.flatted import stringify as _stringify
|
||||||
|
from pyscript.ffi import is_none
|
||||||
|
|
||||||
|
|
||||||
# convert a Python value into an IndexedDB compatible entry
|
# convert a Python value into an IndexedDB compatible entry
|
||||||
def _to_idb(value):
|
def _to_idb(value):
|
||||||
if value is None:
|
if is_none(value):
|
||||||
return _stringify(["null", 0])
|
return _stringify(["null", 0])
|
||||||
if isinstance(value, (bool, float, int, str, list, dict, tuple)):
|
if isinstance(value, (bool, float, int, str, list, dict, tuple)):
|
||||||
return _stringify(["generic", value])
|
return _stringify(["generic", value])
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# from __future__ import annotations # CAUTION: This is not supported in MicroPython.
|
# from __future__ import annotations # CAUTION: This is not supported in MicroPython.
|
||||||
|
|
||||||
from pyscript import document, when, Event # noqa: F401
|
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):
|
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.
|
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.
|
Otherwise, we are being called to *wrap* an existing DOM element.
|
||||||
"""
|
"""
|
||||||
self._dom_element = dom_element or document.createElement(
|
self._dom_element = (
|
||||||
type(self).get_tag_name()
|
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.
|
# HTML on_events attached to the element become pyscript.Event instances.
|
||||||
@@ -124,6 +126,11 @@ class Element:
|
|||||||
# Element instance via `for_`).
|
# Element instance via `for_`).
|
||||||
if name.endswith("_"):
|
if name.endswith("_"):
|
||||||
name = name[:-1] # noqa: FURB188 No str.removesuffix() in MicroPython.
|
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)
|
return getattr(self._dom_element, name)
|
||||||
|
|
||||||
def __setattr__(self, name, value):
|
def __setattr__(self, name, value):
|
||||||
@@ -142,6 +149,11 @@ class Element:
|
|||||||
# Element instance via `for_`).
|
# Element instance via `for_`).
|
||||||
if name.endswith("_"):
|
if name.endswith("_"):
|
||||||
name = name[:-1] # noqa: FURB188 No str.removesuffix() in MicroPython.
|
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_"):
|
if name.startswith("on_"):
|
||||||
# Ensure on-events are cached in the _on_events dict if the
|
# Ensure on-events are cached in the _on_events dict if the
|
||||||
@@ -185,7 +197,7 @@ class Element:
|
|||||||
@property
|
@property
|
||||||
def parent(self):
|
def parent(self):
|
||||||
"""Return the element's `parent `Element`."""
|
"""Return the element's `parent `Element`."""
|
||||||
if self._dom_element.parentElement is None:
|
if is_none(self._dom_element.parentElement):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return Element.wrap_dom_element(self._dom_element.parentElement)
|
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
|
width = width if width is not None else self.videoWidth
|
||||||
height = height if height is not None else self.videoHeight
|
height = height if height is not None else self.videoHeight
|
||||||
|
|
||||||
if to is None:
|
if is_none(to):
|
||||||
to = canvas(width=width, height=height)
|
to = canvas(width=width, height=height)
|
||||||
|
|
||||||
elif isinstance(to, Element):
|
elif isinstance(to, Element):
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import js
|
import js
|
||||||
from pyscript.ffi import create_proxy
|
from pyscript.ffi import create_proxy
|
||||||
from pyscript.util import as_bytearray
|
from pyscript.util import as_bytearray, is_awaitable
|
||||||
|
|
||||||
code = "code"
|
code = "code"
|
||||||
protocols = "protocols"
|
protocols = "protocols"
|
||||||
@@ -8,6 +8,23 @@ reason = "reason"
|
|||||||
methods = ["onclose", "onerror", "onmessage", "onopen"]
|
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:
|
class EventMessage:
|
||||||
def __init__(self, event):
|
def __init__(self, event):
|
||||||
self._event = event
|
self._event = event
|
||||||
@@ -36,20 +53,20 @@ class WebSocket:
|
|||||||
socket = js.WebSocket.new(url, kw[protocols])
|
socket = js.WebSocket.new(url, kw[protocols])
|
||||||
else:
|
else:
|
||||||
socket = js.WebSocket.new(url)
|
socket = js.WebSocket.new(url)
|
||||||
|
|
||||||
|
socket.binaryType = "arraybuffer"
|
||||||
object.__setattr__(self, "_ws", socket)
|
object.__setattr__(self, "_ws", socket)
|
||||||
|
|
||||||
for t in methods:
|
for t in methods:
|
||||||
if t in kw:
|
if t in kw:
|
||||||
# Pyodide fails at setting socket[t] directly
|
add_listener(socket, t, kw[t])
|
||||||
setattr(socket, t, create_proxy(kw[t]))
|
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
return getattr(self._ws, attr)
|
return getattr(self._ws, attr)
|
||||||
|
|
||||||
def __setattr__(self, attr, value):
|
def __setattr__(self, attr, value):
|
||||||
if attr in methods:
|
if attr in methods:
|
||||||
m = lambda e: value(EventMessage(e))
|
add_listener(self._ws, attr, value)
|
||||||
setattr(self._ws, attr, create_proxy(m))
|
|
||||||
else:
|
else:
|
||||||
setattr(self._ws, attr, value)
|
setattr(self._ws, attr, value)
|
||||||
|
|
||||||
|
|||||||
@@ -12,20 +12,33 @@ export default {
|
|||||||
return new Promise(($) => setTimeout($, seconds * 1000));
|
return new Promise(($) => setTimeout($, seconds * 1000));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getFSHandler: (options) => getFileSystemDirectoryHandle(options),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ask a user action via dialog and returns the directory handler once granted.
|
* Ask a user action via dialog and returns the directory handler once granted.
|
||||||
* @param {string} uid
|
* @param {string} uid
|
||||||
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
|
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
|
||||||
* @returns {boolean}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
async storeFSHandler(uid, options = {}) {
|
async storeFSHandler(uid, options = {}) {
|
||||||
if (await idb.has(uid)) return true;
|
if (await idb.has(uid)) return true;
|
||||||
return getFileSystemDirectoryHandle(options).then(
|
return getFileSystemDirectoryHandle(options).then(
|
||||||
async (handler) => {
|
async (handler) => {
|
||||||
await idb.set(uid, handler);
|
await idb.set(uid, { handler, options });
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
() => false,
|
() => 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
|
classList = document.documentElement.classList
|
||||||
|
|
||||||
if not __terminal__:
|
if not __terminal__: # noqa: F821 __terminal__ is defined in core/src/plugins/donkey.js
|
||||||
classList.add("error")
|
classList.add("error")
|
||||||
else:
|
else:
|
||||||
classList.add("ok")
|
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)
|
print(event.type)
|
||||||
ws.send("hello")
|
ws.send("hello")
|
||||||
|
|
||||||
def onmessage(event):
|
async def onmessage(event):
|
||||||
print(event.type, event.data)
|
print(event.type, event.data)
|
||||||
ws.close()
|
ws.close()
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,13 @@ import { test, expect } from '@playwright/test';
|
|||||||
|
|
||||||
test.setTimeout(120 * 1000);
|
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 }) => {
|
test('MicroPython display', async ({ page }) => {
|
||||||
await page.goto('http://localhost:8080/tests/javascript/mpy.html');
|
await page.goto('http://localhost:8080/tests/javascript/mpy.html');
|
||||||
await page.waitForSelector('html.done.worker');
|
await page.waitForSelector('html.done.worker');
|
||||||
@@ -59,6 +66,11 @@ test('MicroPython + configURL', async ({ page }) => {
|
|||||||
await page.waitForSelector('html.main.worker');
|
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 }) => {
|
test('Pyodide + terminal on Main', async ({ page }) => {
|
||||||
await page.goto('http://localhost:8080/tests/javascript/py-terminal-main.html');
|
await page.goto('http://localhost:8080/tests/javascript/py-terminal-main.html');
|
||||||
await page.waitForSelector('html.ok');
|
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.sync("/persistent")
|
||||||
|
|
||||||
|
# await fs.revoke("/persistent")
|
||||||
|
|
||||||
elif not RUNNING_IN_WORKER:
|
elif not RUNNING_IN_WORKER:
|
||||||
from pyscript import document
|
from pyscript import document
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ elif not RUNNING_IN_WORKER:
|
|||||||
js.alert("unable to grant access")
|
js.alert("unable to grant access")
|
||||||
|
|
||||||
async def unmount(event):
|
async def unmount(event):
|
||||||
await fs.unmount("/persistent")
|
await fs.revoke("/persistent")
|
||||||
button.textContent = "mount"
|
button.textContent = "mount"
|
||||||
button.onclick = mount
|
button.onclick = mount
|
||||||
|
|
||||||
|
|||||||
@@ -318,16 +318,12 @@ async def main(winstyle=0):
|
|||||||
if not fullscreen:
|
if not fullscreen:
|
||||||
print("Changing to FULLSCREEN")
|
print("Changing to FULLSCREEN")
|
||||||
screen_backup = screen.copy()
|
screen_backup = screen.copy()
|
||||||
screen = pygame.display.set_mode(
|
screen = pygame.display.set_mode(SCREENRECT.size, winstyle | pygame.FULLSCREEN)
|
||||||
SCREENRECT.size, winstyle | pygame.FULLSCREEN, bestdepth
|
|
||||||
)
|
|
||||||
screen.blit(screen_backup, (0, 0))
|
screen.blit(screen_backup, (0, 0))
|
||||||
else:
|
else:
|
||||||
print("Changing to windowed mode")
|
print("Changing to windowed mode")
|
||||||
screen_backup = screen.copy()
|
screen_backup = screen.copy()
|
||||||
screen = pygame.display.set_mode(
|
screen = pygame.display.set_mode(SCREENRECT.size, winstyle)
|
||||||
SCREENRECT.size, winstyle, bestdepth
|
|
||||||
)
|
|
||||||
screen.blit(screen_backup, (0, 0))
|
screen.blit(screen_backup, (0, 0))
|
||||||
pygame.display.flip()
|
pygame.display.flip()
|
||||||
fullscreen = not fullscreen
|
fullscreen = not fullscreen
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ def get_stats_gl(renderer):
|
|||||||
|
|
||||||
def bg_from_v(*vertices):
|
def bg_from_v(*vertices):
|
||||||
geometry = new(THREE.BufferGeometry)
|
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)
|
attr = new(THREE.Float32BufferAttribute, vertices_f32a, 3)
|
||||||
return geometry.setAttribute('position', attr)
|
return geometry.setAttribute('position', attr)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from pyscript import document, window, PyWorker
|
|||||||
from libthree import THREE, clear, SoundPlayer
|
from libthree import THREE, clear, SoundPlayer
|
||||||
from libthree import get_renderer, get_ortho_camera
|
from libthree import get_renderer, get_ortho_camera
|
||||||
from libthree import get_loading_manager, get_stats_gl
|
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 libfft import BeatSync
|
||||||
|
|
||||||
from multipyjs import MICROPYTHON, new, call, to_js, create_proxy
|
from multipyjs import MICROPYTHON, new, call, to_js, create_proxy
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="stylesheet" href="../../dist/core.css">
|
<link rel="stylesheet" href="../../../dist/core.css">
|
||||||
<script type="module" src="../../dist/core.js"></script>
|
<script type="module" src="../../../dist/core.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script type="py-editor">
|
<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"
|
"./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
|
# The function should have been called when the whenable object was
|
||||||
# triggered.
|
# triggered.
|
||||||
assert counter == 1
|
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)
|
@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
|
The "hello" function in the example_js_worker_module.js file is available
|
||||||
via the js_modules object while running in a worker.
|
via the js_modules object while running in a worker.
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
Tests for the PyScript media module.
|
Tests for the PyScript media module.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pyscript import media
|
|
||||||
import upytest
|
import upytest
|
||||||
|
|
||||||
from pyscript import media
|
from pyscript import media
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ def test_as_bytearray():
|
|||||||
msg = b"Hello, world!"
|
msg = b"Hello, world!"
|
||||||
buffer = js.ArrayBuffer.new(len(msg))
|
buffer = js.ArrayBuffer.new(len(msg))
|
||||||
ui8a = js.Uint8Array.new(buffer)
|
ui8a = js.Uint8Array.new(buffer)
|
||||||
for b in msg:
|
for i, b in enumerate(msg):
|
||||||
ui8a[i] = b
|
ui8a[i] = b
|
||||||
ba = util.as_bytearray(buffer)
|
ba = util.as_bytearray(buffer)
|
||||||
assert isinstance(ba, bytearray)
|
assert isinstance(ba, bytearray)
|
||||||
|
|||||||
@@ -871,7 +871,17 @@ class TestElements:
|
|||||||
self._create_el_and_basic_asserts("kbd", "some text")
|
self._create_el_and_basic_asserts("kbd", "some text")
|
||||||
|
|
||||||
def test_label(self):
|
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):
|
def test_legend(self):
|
||||||
self._create_el_and_basic_asserts("legend", "some text")
|
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 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 };
|
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.
|
* Ask a user action via dialog and returns the directory handler once granted.
|
||||||
* @param {string} uid
|
* @param {string} uid
|
||||||
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
|
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
|
||||||
* @returns {boolean}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
function storeFSHandler(uid: string, options?: {
|
function storeFSHandler(uid: string, options?: {
|
||||||
id?: string;
|
id?: string;
|
||||||
mode?: "read" | "readwrite";
|
mode?: "read" | "readwrite";
|
||||||
hint?: "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
|
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;
|
export default _default;
|
||||||
|
|||||||
@@ -137,12 +137,14 @@
|
|||||||
<body>
|
<body>
|
||||||
<h1>Hello world!</h1>
|
<h1>Hello world!</h1>
|
||||||
<p>These are the Python interpreters in PyScript _VERSION_:</p>
|
<p>These are the Python interpreters in PyScript _VERSION_:</p>
|
||||||
<script type="py"> <!-- Pyodide -->
|
<script type="py">
|
||||||
|
# Pyodide
|
||||||
from pyscript import display
|
from pyscript import display
|
||||||
import sys
|
import sys
|
||||||
display(sys.version)
|
display(sys.version)
|
||||||
</script>
|
</script>
|
||||||
<script type="mpy"> <!-- MicroPython -->
|
<script type="mpy">
|
||||||
|
# MicroPython
|
||||||
from pyscript import display
|
from pyscript import display
|
||||||
import sys
|
import sys
|
||||||
display(sys.version)
|
display(sys.version)
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ skip = "*.js,*.json"
|
|||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 114
|
line-length = 114
|
||||||
lint.select = ["C4", "C90", "E", "EM", "F", "PIE", "PYI", "PLC", "Q", "RET", "W"]
|
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
|
lint.mccabe.max-complexity = 27
|
||||||
|
|||||||
Reference in New Issue
Block a user