Compare commits

...

30 Commits

Author SHA1 Message Date
Andrea Giammarchi
66966a732e Updated Polyscript to its latest (#2392)
* Updated Polyscript to its latest

* forgot to update the README for the next release
2025-10-21 12:53:35 +02:00
Andrea Giammarchi
ec090922cb Fix #2372 - Allow custom TOML parser (#2390)
* Fix #2372 - Allow custom TOML parser

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-10-10 17:07:36 +02:00
Andrea Giammarchi
f769f215b2 Updated Pyodide to its latest (#2389) 2025-10-08 12:35:21 +02:00
Nicholas Tollervey
ffc78ab6a2 Remove superfluous code now MicroPython supports inspect API for function signature inspection. (#2387)
* Remove superfluous code now MicroPython supports inspect API for function signature inspection. 
* Added test to ensure all callables are covered.
2025-10-08 09:27:53 +01:00
Jeremy Kawahara
b609b605f5 Fix py-editor execute code on ctrl-enter (#2385)
* Fix dist path

* Remove defaultKeymap

* Return true from listener

* Put defaultKeymap after custom key map
2025-10-07 22:57:14 +02:00
Andrea Giammarchi
100a1e4bc1 Updated MicroPython one more time (#2386) 2025-10-07 10:57:29 +02:00
pre-commit-ci[bot]
c848061a44 [pre-commit.ci] pre-commit autoupdate (#2384)
updates:
- https://github.com/psf/blackhttps://github.com/psf/black-pre-commit-mirror
- [github.com/psf/black-pre-commit-mirror: 25.1.0 → 25.9.0](https://github.com/psf/black-pre-commit-mirror/compare/25.1.0...25.9.0)
- [github.com/astral-sh/ruff-pre-commit: v0.12.11 → v0.13.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.11...v0.13.3)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-10-06 21:43:10 -04:00
Andrea Giammarchi
2647e78480 Updated polyscript to bring in latest MicroPython (#2383)
* Updated polyscript to bring in latest MicroPython

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-10-06 11:13:10 +02:00
Andrea Giammarchi
482d57c27c Amend on MicroPython latest (#2382) 2025-10-02 15:32:38 +02:00
Andrea Giammarchi
4ce989acf3 Updated Polyscript (#2376) 2025-10-02 13:43:24 +02:00
Andrea Giammarchi
1e62d0b1fe Follow up on autostart (#2380) 2025-09-30 15:46:37 +02:00
dependabot[bot]
2d3ad0ab2d Bump the github-actions group with 2 updates (#2378)
Bumps the github-actions group with 2 updates: [actions/setup-node](https://github.com/actions/setup-node) and [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials).


Updates `actions/setup-node` from 4 to 5
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v5)

Updates `aws-actions/configure-aws-credentials` from 4 to 5
- [Release notes](https://github.com/aws-actions/configure-aws-credentials/releases)
- [Changelog](https://github.com/aws-actions/configure-aws-credentials/blob/main/CHANGELOG.md)
- [Commits](https://github.com/aws-actions/configure-aws-credentials/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: aws-actions/configure-aws-credentials
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-15 09:53:25 +02:00
Andrea Giammarchi
3657492c52 Simplify even further the bridge with a fallback (#2379) 2025-09-12 09:40:23 +02:00
pre-commit-ci[bot]
a8b8e1de36 [pre-commit.ci] pre-commit autoupdate (#2377)
updates:
- [github.com/pre-commit/pre-commit-hooks: v5.0.0 → v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0)
- [github.com/astral-sh/ruff-pre-commit: v0.12.8 → v0.12.11](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.8...v0.12.11)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-09-01 21:31:15 -04:00
Andrea Giammarchi
726009029a Updated Pyodide to its 0.28.2 version (#2374) 2025-08-21 16:51:25 -04:00
Christian Clauss
8b35304ab4 Fix undefined names in Python code (#2371)
Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
2025-08-18 13:57:33 +02:00
dependabot[bot]
9e4cb44d73 Bump actions/checkout from 4 to 5 in the github-actions group (#2373)
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 4 to 5
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
2025-08-18 13:51:33 +02:00
Christian Clauss
4bf3651c9a pre-commit: Upgrade the Python linter ruff (#2370) 2025-08-18 13:36:36 +02:00
Andrea Giammarchi
67fa31e4ea Bumped version to 2025.8.1 (#2369) 2025-08-07 09:59:11 +02:00
Andrea Giammarchi
4937a46731 Updated Polyscript to its latest (#2364)
* Fix #2360 - Better shared env/setup handling (#2361)

* Updated Polyscript to its latest

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* changed is_null to a more Pythonic is_none

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-08-06 19:23:24 +02:00
Andrea Giammarchi
b4e9a3093c Fix #2338 - Added explicit fs.revoke(path) (#2368) 2025-08-06 14:40:52 +02:00
Andrea Giammarchi
a129be8136 WebSocket and PyWorker fixes (#2366)
* WebSocket and PyWorker fixes

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-07-24 15:03:51 +02:00
Andrea Giammarchi
eaa6711756 Fix #2360 - Better shared env/setup handling (#2361) 2025-07-11 10:56:14 +02:00
Andrea Giammarchi
b528ba67a9 Intermediate release with async worker handler fixes (#2359) 2025-07-10 15:21:50 +02:00
Andrea Giammarchi
71ad1a40cb Update Polyscript with latest Micropython (#2357) 2025-07-04 09:34:12 +02:00
Andrea Giammarchi
e433275938 Readme update (#2356)
* Updated the README with latest PyScript version

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-07-01 14:01:51 +02:00
Andrea Giammarchi
87256a662b Updated Polyscript to its latest (#2355)
* Updated Polyscript to its latest
* added tests for `experimental_ffi_timeout`
2025-07-01 13:07:28 +02:00
Andrea Giammarchi
7336ae545e The PyScript Bridge Helper (#2353)
* The PyScript Bridge Helper

* added importmap to test latest versions with ease
2025-06-26 12:41:29 +02:00
Nicholas Tollervey
d68260c0c7 Fix a bug in <label> handling where 'for_' attribute should be 'htmlFor' on underlying HTML element. (#2352)
* Fix bug in label handling where 'for_' attribute should be 'htmlFor' on underlying HTML element.

* Fix comment.
2025-06-18 15:01:33 +01:00
Nicholas Tollervey
14cc05fb80 Fix code example problem in the release HTML. (#2345) 2025-05-21 16:27:27 +01:00
58 changed files with 1269 additions and 581 deletions

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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

View File

@@ -1,3 +1,4 @@
ISSUE_TEMPLATE
*.min.*
package-lock.json
bridge/

View File

@@ -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
View 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
View 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
View 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
View 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>

View 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>

View File

@@ -0,0 +1,5 @@
import sys
def version():
return sys.version

18
bridge/test/test.js Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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": {

View File

@@ -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

View File

@@ -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 });
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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):

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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])

View File

@@ -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):

View File

@@ -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)

View File

@@ -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

View File

@@ -0,0 +1 @@
print("OK")

View 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>

View File

@@ -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")

View 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>

View File

@@ -14,7 +14,7 @@
print(event.type)
ws.send("hello")
def onmessage(event):
async def onmessage(event):
print(event.type, event.data)
ws.close()

View File

@@ -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');

View 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>

View File

@@ -0,0 +1,3 @@
from pyscript import document, window
document.body.append(window.Date.now() - window.start)

View File

@@ -0,0 +1,2 @@
experimental_ffi_timeout = 0
package_cache = "passthrough"

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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">

View 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>

View File

@@ -0,0 +1,3 @@
experimental_remote_packages = true
packages = ['https://webreflection.github.io/examples/pyscript/module/package.toml']

View File

@@ -24,5 +24,6 @@
"./example_js_worker_module.js": "greeting_worker"
}
},
"packages": ["Pillow" ]
"packages": ["Pillow" ],
"experimental_ffi_timeout": 0
}

View File

@@ -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

View File

@@ -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.

View File

@@ -2,7 +2,6 @@
Tests for the PyScript media module.
"""
from pyscript import media
import upytest
from pyscript import media

View File

@@ -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)

View File

@@ -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")

View File

@@ -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
View File

@@ -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;

View File

@@ -137,12 +137,14 @@
&lt;body&gt;
&lt;h1&gt;Hello world!&lt;/h1&gt;
&lt;p&gt;These are the Python interpreters in PyScript _VERSION_:&lt;/p&gt;
&lt;script type=&quot;py&quot;&gt; &lt;!-- Pyodide --&gt;
&lt;script type=&quot;py&quot;&gt;
# Pyodide
from pyscript import display
import sys
display(sys.version)
&lt;/script&gt;
&lt;script type=&quot;mpy&quot;&gt; &lt;!-- MicroPython --&gt;
&lt;script type=&quot;mpy&quot;&gt;
# MicroPython
from pyscript import display
import sys
display(sys.version)

View File

@@ -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