mirror of
https://github.com/pyscript/pyscript.git
synced 2026-02-28 14:01:35 -05:00
pyscript API/docstring refactor with comprehensive tests (#2414)
* Revise display module. TODO: more comprehensive tests. Especially around mimebundles. * Markdown corrections in example code in display.py docstrings. * Minor adjustments and a much more comprehensive test-suite for the display module. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Updated docstring in __init__.py. * Remove unused imports and black-ify the source. * Refactor, docs and tests for the Event class in events.py. * Refactored, simplified and documented @when decorator. * Extensive test suite for @when decorator. * Documentation and minor refactoring of the fetch.py module. TODO: Check tests. * Refactored and more comprehensive tests for the fetch module. * Add/clarify Event related interactions. Thanks @Neon22 for the suggestion. * Refactor, document ffi.py module. * More complete passing tests for ffi.py. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add docstrings to flatted.py. Since this is actually an external(ish) module, tests for it should be in the external repository from which this code is derived. * Minor docstring cleanup in ffi.py. * Added docstrings and clarifications to fs.py. * Add very limited test suite for fs.py. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Rename magic_js.py to context.py, add comprehensive docstrings, and rename certain internal things for readability and comprehension. * Fix dict check in ffi.py. * Rename test_js_modules to test_context. * Fix test configuration aftert rename. * Docs and refactor of media.py. * Comprehensive tests for media.py. * Refactor and docstrings for storage.py * Appease the ruff gods. * Further storage.py changes and a more complete test suite for storage. * Refactor and docstrings for the util.py module. Fixed a problem with is_awaitable not handling async bound methods. * More comprehensive test suite for util.py. Updated to latest upytest. * A major refactoring, documenting and simplification of the web.py module substantially reducing it in size and complexity with only a few minor (edge) behavioural changes. Softly breaking changes include: - An element's classes are just a set. - An element's styles are just a dict. - Explicitly use `update_all` with ElementCollections (simpler and greater flexibility). - Extract a child element by id with `my_container["#an-id"]` * Updates and additions for a more comprehensive test suite for the web.py module. All code paths are exercised and checked. * Black tidy-ups in test suite. * Refactor and documentation for websocket.py module. * Tests for websocket.py. Disabled due to playwright flakiness, but they all pass in a local browser. * Refactor and documentation of workers.py module. * Added tests for workers.py module. Updated related test suite to account for the new named worker in the test HTML. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Refactor away remaining "is not None" not caught before. * Remove check-docstring-first because it interferes with the auto-generated documentation (where triple quoted strings are used to document module attributes). * Careful Markdown changes so the docstrings render properly in the PyScript docs. * Typo correction. * More typo corrections and clarifications. * Add clarification about SVG handling to _render_image docstring. * Add DOM event options to the @when decorator (with new tests to exercise this functionality). * Fixes default value for options if no options passed into @when. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
90ae3cea95
commit
a02ff691d2
File diff suppressed because one or more lines are too long
@@ -1,36 +1,105 @@
|
||||
# Some notes about the naming conventions and the relationship between various
|
||||
# similar-but-different names.
|
||||
#
|
||||
# import pyscript
|
||||
# this package contains the main user-facing API offered by pyscript. All
|
||||
# the names which are supposed be used by end users should be made
|
||||
# available in pyscript/__init__.py (i.e., this file)
|
||||
#
|
||||
# import _pyscript
|
||||
# this is an internal module implemented in JS. It is used internally by
|
||||
# the pyscript package, end users should not use it directly. For its
|
||||
# implementation, grep for `interpreter.registerJsModule("_pyscript",
|
||||
# ...)` in core.js
|
||||
#
|
||||
# import js
|
||||
# this is the JS globalThis, as exported by pyodide and/or micropython's
|
||||
# FFIs. As such, it contains different things in the main thread or in a
|
||||
# worker.
|
||||
#
|
||||
# import pyscript.magic_js
|
||||
# this submodule abstracts away some of the differences between the main
|
||||
# thread and the worker. In particular, it defines `window` and `document`
|
||||
# in such a way that these names work in both cases: in the main thread,
|
||||
# they are the "real" objects, in the worker they are proxies which work
|
||||
# thanks to coincident.
|
||||
#
|
||||
# from pyscript import window, document
|
||||
# these are just the window and document objects as defined by
|
||||
# pyscript.magic_js. This is the blessed way to access them from pyscript,
|
||||
# as it works transparently in both the main thread and worker cases.
|
||||
"""
|
||||
This is the main `pyscript` namespace. It provides the primary Pythonic API
|
||||
for users to interact with the
|
||||
[browser's own API](https://developer.mozilla.org/en-US/docs/Web/API). It
|
||||
includes utilities for common activities such as displaying content, handling
|
||||
events, fetching resources, managing local storage, and coordinating with
|
||||
web workers.
|
||||
|
||||
The most important names provided by this namespace can be directly imported
|
||||
from `pyscript`, for example:
|
||||
|
||||
```python
|
||||
from pyscript import display, HTML, fetch, when, storage, WebSocket
|
||||
```
|
||||
|
||||
The following names are available in the `pyscript` namespace:
|
||||
|
||||
- `RUNNING_IN_WORKER`: Boolean indicating if the code is running in a Web
|
||||
Worker.
|
||||
- `PyWorker`: Class for creating Web Workers running Python code.
|
||||
- `config`: Configuration object for pyscript settings.
|
||||
- `current_target`: The element in the DOM that is the current target for
|
||||
output.
|
||||
- `document`: The standard `document` object, proxied in workers.
|
||||
- `window`: The standard `window` object, proxied in workers.
|
||||
- `js_import`: Function to dynamically import JS modules.
|
||||
- `js_modules`: Object containing JS modules available to Python.
|
||||
- `sync`: Utility for synchronizing between worker and main thread.
|
||||
- `display`: Function to render Python objects in the web page.
|
||||
- `HTML`: Helper class to create HTML content for display.
|
||||
- `fetch`: Function to perform HTTP requests.
|
||||
- `Storage`: Class representing browser storage (local/session).
|
||||
- `storage`: Object to interact with browser's local storage.
|
||||
- `WebSocket`: Class to create and manage WebSocket connections.
|
||||
- `when`: Function to register event handlers on DOM elements.
|
||||
- `Event`: Class representing user defined or DOM events.
|
||||
- `py_import`: Function to lazily import Pyodide related Python modules.
|
||||
|
||||
If running in the main thread, the following additional names are available:
|
||||
|
||||
- `create_named_worker`: Function to create a named Web Worker.
|
||||
- `workers`: Object to manage and interact with existing Web Workers.
|
||||
|
||||
All of these names are defined in the various submodules of `pyscript` and
|
||||
are imported and re-exported here for convenience. Please refer to the
|
||||
respective submodule documentation for more details on each component.
|
||||
|
||||
|
||||
!!! Note
|
||||
Some notes about the naming conventions and the relationship between
|
||||
various similar-but-different names found within this code base.
|
||||
|
||||
```python
|
||||
import pyscript
|
||||
```
|
||||
|
||||
The `pyscript` package contains the main user-facing API offered by
|
||||
PyScript. All the names which are supposed be used by end users should
|
||||
be made available in `pyscript/__init__.py` (i.e., this source file).
|
||||
|
||||
```python
|
||||
import _pyscript
|
||||
```
|
||||
|
||||
The `_pyscript` module is an internal API implemented in JS. **End users
|
||||
should not use it directly**. For its implementation, grep for
|
||||
`interpreter.registerJsModule("_pyscript",...)` in `core.js`.
|
||||
|
||||
```python
|
||||
import js
|
||||
```
|
||||
|
||||
The `js` object is
|
||||
[the JS `globalThis`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis),
|
||||
as exported by Pyodide and/or Micropython's foreign function interface
|
||||
(FFI). As such, it contains different things in the main thread or in a
|
||||
worker, as defined by web standards.
|
||||
|
||||
```python
|
||||
import pyscript.context
|
||||
```
|
||||
|
||||
The `context` submodule abstracts away some of the differences between
|
||||
the main thread and a worker. Its most important features are made
|
||||
available in the root `pyscript` namespace. All other functionality is
|
||||
mostly for internal PyScript use or advanced users. In particular, it
|
||||
defines `window` and `document` in such a way that these names work in
|
||||
both cases: in the main thread, they are the "real" objects, in a worker
|
||||
they are proxies which work thanks to
|
||||
[coincident](https://github.com/WebReflection/coincident).
|
||||
|
||||
```python
|
||||
from pyscript import window, document
|
||||
```
|
||||
|
||||
These are just the `window` and `document` objects as defined by
|
||||
`pyscript.context`. This is the blessed way to access them from `pyscript`,
|
||||
as it works transparently in both the main thread and worker cases.
|
||||
"""
|
||||
|
||||
from polyscript import lazy_py_modules as py_import
|
||||
from pyscript.magic_js import (
|
||||
from pyscript.context import (
|
||||
RUNNING_IN_WORKER,
|
||||
PyWorker,
|
||||
config,
|
||||
|
||||
198
core/src/stdlib/pyscript/context.py
Normal file
198
core/src/stdlib/pyscript/context.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""
|
||||
Execution context management for PyScript.
|
||||
|
||||
This module handles the differences between running in the
|
||||
[main browser thread](https://developer.mozilla.org/en-US/docs/Glossary/Main_thread)
|
||||
versus running in a
|
||||
[Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers),
|
||||
providing a consistent API regardless of the execution context.
|
||||
|
||||
Key features:
|
||||
|
||||
- Detects whether code is running in a worker or main thread. Read this via
|
||||
the boolean `pyscript.context.RUNNING_IN_WORKER`.
|
||||
- Parses and normalizes configuration from `polyscript.config` and adds the
|
||||
Python interpreter type via the `type` key in `pyscript.context.config`.
|
||||
- Provides appropriate implementations of `window`, `document`, and `sync`.
|
||||
- Sets up JavaScript module import system, including a lazy `js_import`
|
||||
function.
|
||||
- Manages `PyWorker` creation.
|
||||
- Provides access to the current display target via
|
||||
`pyscript.context.display_target`.
|
||||
|
||||
!!! warning
|
||||
|
||||
These are key differences between the main thread and worker contexts:
|
||||
|
||||
Main thread context:
|
||||
|
||||
- `window` and `document` are available directly.
|
||||
- `PyWorker` can be created to spawn worker threads.
|
||||
- `sync` is not available (raises `NotSupported`).
|
||||
|
||||
Worker context:
|
||||
|
||||
- `window` and `document` are proxied from main thread (if SharedArrayBuffer
|
||||
available).
|
||||
- `PyWorker` is not available (raises `NotSupported`).
|
||||
- `sync` utilities are available for main thread communication.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
import js
|
||||
from polyscript import config as _polyscript_config
|
||||
from polyscript import js_modules
|
||||
from pyscript.util import NotSupported
|
||||
|
||||
RUNNING_IN_WORKER = not hasattr(js, "document")
|
||||
"""Detect execution context: True if running in a worker, False if main thread."""
|
||||
|
||||
config = json.loads(js.JSON.stringify(_polyscript_config))
|
||||
"""Parsed and normalized configuration."""
|
||||
if isinstance(config, str):
|
||||
config = {}
|
||||
|
||||
js_import = None
|
||||
"""Function to import JavaScript modules dynamically."""
|
||||
|
||||
window = None
|
||||
"""The `window` object (proxied if in a worker)."""
|
||||
|
||||
document = None
|
||||
"""The `document` object (proxied if in a worker)."""
|
||||
|
||||
sync = None
|
||||
"""Sync utilities for worker-main thread communication (only in workers)."""
|
||||
|
||||
# Detect and add Python interpreter type to config.
|
||||
if "MicroPython" in sys.version:
|
||||
config["type"] = "mpy"
|
||||
else:
|
||||
config["type"] = "py"
|
||||
|
||||
|
||||
class _JSModuleProxy:
|
||||
"""
|
||||
Proxy for JavaScript modules imported via js_modules.
|
||||
|
||||
This allows Python code to import JavaScript modules using Python's
|
||||
import syntax:
|
||||
|
||||
```python
|
||||
from pyscript.js_modules lodash import debounce
|
||||
```
|
||||
|
||||
The proxy lazily retrieves the actual JavaScript module when accessed.
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
"""
|
||||
Create a proxy for the named JavaScript module.
|
||||
"""
|
||||
self.name = name
|
||||
|
||||
def __getattr__(self, field):
|
||||
"""
|
||||
Retrieve a JavaScript object/function from the proxied JavaScript
|
||||
module via the given `field` name.
|
||||
"""
|
||||
# Avoid Pyodide looking for non-existent special methods.
|
||||
if not field.startswith("_"):
|
||||
return getattr(getattr(js_modules, self.name), field)
|
||||
return None
|
||||
|
||||
|
||||
# Register all available JavaScript modules in Python's module system.
|
||||
# This enables: from pyscript.js_modules.xxx import yyy
|
||||
for module_name in js.Reflect.ownKeys(js_modules):
|
||||
sys.modules[f"pyscript.js_modules.{module_name}"] = _JSModuleProxy(module_name)
|
||||
sys.modules["pyscript.js_modules"] = js_modules
|
||||
|
||||
|
||||
# Context-specific setup: Worker vs Main Thread.
|
||||
if RUNNING_IN_WORKER:
|
||||
import polyscript
|
||||
|
||||
# PyWorker cannot be created from within a worker.
|
||||
PyWorker = NotSupported(
|
||||
"pyscript.PyWorker",
|
||||
"pyscript.PyWorker works only when running in the main thread",
|
||||
)
|
||||
|
||||
# Attempt to access main thread's window and document via SharedArrayBuffer.
|
||||
try:
|
||||
window = polyscript.xworker.window
|
||||
document = window.document
|
||||
js.document = document
|
||||
|
||||
# Create js_import function that runs imports on the main thread.
|
||||
js_import = window.Function(
|
||||
"return (...urls) => Promise.all(urls.map((url) => import(url)))"
|
||||
)()
|
||||
|
||||
except:
|
||||
# SharedArrayBuffer not available - window/document cannot be proxied.
|
||||
sab_error_message = (
|
||||
"Unable to use `window` or `document` in worker. "
|
||||
"This requires SharedArrayBuffer support. "
|
||||
"See: https://docs.pyscript.net/latest/faq/#sharedarraybuffer"
|
||||
)
|
||||
js.console.warn(sab_error_message)
|
||||
window = NotSupported("pyscript.window", sab_error_message)
|
||||
document = NotSupported("pyscript.document", sab_error_message)
|
||||
|
||||
# Worker-specific utilities for main thread communication.
|
||||
sync = polyscript.xworker.sync
|
||||
|
||||
def current_target():
|
||||
"""
|
||||
Get the current output target in worker context.
|
||||
"""
|
||||
return polyscript.target
|
||||
|
||||
else:
|
||||
# Main thread context setup.
|
||||
import _pyscript
|
||||
from _pyscript import PyWorker as _PyWorker
|
||||
from pyscript.ffi import to_js
|
||||
|
||||
js_import = _pyscript.js_import
|
||||
|
||||
def PyWorker(url, **options):
|
||||
"""
|
||||
Create a Web Worker running Python code.
|
||||
|
||||
This spawns a new worker thread that can execute Python code
|
||||
found at the `url`, independently of the main thread. The
|
||||
`**options` can be used to configure the worker.
|
||||
|
||||
```python
|
||||
from pyscript import PyWorker
|
||||
|
||||
|
||||
# Create a worker to run background tasks.
|
||||
# (`type` MUST be either `micropython` or `pyodide`)
|
||||
worker = PyWorker("./worker.py", type="micropython")
|
||||
```
|
||||
|
||||
PyWorker **can only be created from the main thread**, not from
|
||||
within another worker.
|
||||
"""
|
||||
return _PyWorker(url, to_js(options))
|
||||
|
||||
# Main thread has direct access to window and document.
|
||||
window = js
|
||||
document = js.document
|
||||
|
||||
# sync is not available in main thread (only in workers).
|
||||
sync = NotSupported(
|
||||
"pyscript.sync", "pyscript.sync works only when running in a worker"
|
||||
)
|
||||
|
||||
def current_target():
|
||||
"""
|
||||
Get the current output target in main thread context.
|
||||
"""
|
||||
return _pyscript.target
|
||||
@@ -1,64 +1,104 @@
|
||||
"""
|
||||
Display Pythonic content in the browser.
|
||||
|
||||
This module provides the `display()` function for rendering Python objects
|
||||
in the web page. The function introspects objects to determine the appropriate
|
||||
[MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types/Common_types)
|
||||
and rendering method.
|
||||
|
||||
Supported MIME types:
|
||||
|
||||
- `text/plain`: Plain text (HTML-escaped)
|
||||
- `text/html`: HTML content
|
||||
- `image/png`: PNG images as data URLs
|
||||
- `image/jpeg`: JPEG images as data URLs
|
||||
- `image/svg+xml`: SVG graphics
|
||||
- `application/json`: JSON data
|
||||
- `application/javascript`: JavaScript code (discouraged)
|
||||
|
||||
The `display()` function uses standard Python representation methods
|
||||
(`_repr_html_`, `_repr_png_`, etc.) to determine how to render objects.
|
||||
Objects can provide a `_repr_mimebundle_` method to specify preferred formats
|
||||
like this:
|
||||
|
||||
```python
|
||||
def _repr_mimebundle_(self):
|
||||
return {
|
||||
"text/html": "<b>Bold HTML</b>",
|
||||
"image/png": "<base64-encoded-png-data>",
|
||||
}
|
||||
```
|
||||
|
||||
Heavily inspired by
|
||||
[IPython's rich display system](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html).
|
||||
"""
|
||||
|
||||
import base64
|
||||
import html
|
||||
import io
|
||||
import re
|
||||
|
||||
from pyscript.magic_js import current_target, document, window
|
||||
from collections import OrderedDict
|
||||
from pyscript.context import current_target, document, window
|
||||
from pyscript.ffi import is_none
|
||||
|
||||
_MIME_METHODS = {
|
||||
"savefig": "image/png",
|
||||
"_repr_javascript_": "application/javascript",
|
||||
"_repr_json_": "application/json",
|
||||
"_repr_latex": "text/latex",
|
||||
"_repr_png_": "image/png",
|
||||
"_repr_jpeg_": "image/jpeg",
|
||||
"_repr_pdf_": "application/pdf",
|
||||
"_repr_svg_": "image/svg+xml",
|
||||
"_repr_markdown_": "text/markdown",
|
||||
"_repr_html_": "text/html",
|
||||
"__repr__": "text/plain",
|
||||
}
|
||||
|
||||
|
||||
def _render_image(mime, value, meta):
|
||||
# If the image value is using bytes we should convert it to base64
|
||||
# otherwise it will return raw bytes and the browser will not be able to
|
||||
# render it.
|
||||
"""
|
||||
Render image (`mime`) data (`value`) as an HTML img element with data URL.
|
||||
Any `meta` attributes are added to the img tag.
|
||||
|
||||
Accepts both raw bytes and base64-encoded strings for flexibility. This
|
||||
only handles PNG and JPEG images. SVG images are handled separately as
|
||||
their raw XML content (which the browser can render directly).
|
||||
"""
|
||||
if isinstance(value, bytes):
|
||||
value = base64.b64encode(value).decode("utf-8")
|
||||
|
||||
# This is the pattern of base64 strings
|
||||
base64_pattern = re.compile(
|
||||
r"^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$"
|
||||
)
|
||||
# If value doesn't match the base64 pattern we should encode it to base64
|
||||
if len(value) > 0 and not base64_pattern.match(value):
|
||||
value = base64.b64encode(value.encode("utf-8")).decode("utf-8")
|
||||
|
||||
data = f"data:{mime};charset=utf-8;base64,{value}"
|
||||
attrs = " ".join(['{k}="{v}"' for k, v in meta.items()])
|
||||
return f'<img src="{data}" {attrs}></img>'
|
||||
attrs = "".join([f' {k}="{v}"' for k, v in meta.items()])
|
||||
return f'<img src="data:{mime};base64,{value}"{attrs}>'
|
||||
|
||||
|
||||
def _identity(value, meta):
|
||||
return value
|
||||
|
||||
|
||||
_MIME_RENDERERS = {
|
||||
"text/plain": html.escape,
|
||||
"text/html": _identity,
|
||||
"image/png": lambda value, meta: _render_image("image/png", value, meta),
|
||||
"image/jpeg": lambda value, meta: _render_image("image/jpeg", value, meta),
|
||||
"image/svg+xml": _identity,
|
||||
"application/json": _identity,
|
||||
"application/javascript": lambda value, meta: f"<script>{value}<\\/script>",
|
||||
# Maps MIME types to rendering functions.
|
||||
_MIME_TO_RENDERERS = {
|
||||
"text/plain": lambda v, m: html.escape(v),
|
||||
"text/html": lambda v, m: v,
|
||||
"image/png": lambda v, m: _render_image("image/png", v, m),
|
||||
"image/jpeg": lambda v, m: _render_image("image/jpeg", v, m),
|
||||
"image/svg+xml": lambda v, m: v,
|
||||
"application/json": lambda v, m: v,
|
||||
"application/javascript": lambda v, m: f"<script>{v}<\\/script>",
|
||||
}
|
||||
|
||||
|
||||
# Maps Python representation methods to MIME types. This is an ordered dict
|
||||
# because the order defines preference when multiple methods are available,
|
||||
# and MicroPython's limited dicts don't preserve insertion order.
|
||||
_METHOD_TO_MIME = OrderedDict(
|
||||
[
|
||||
("savefig", "image/png"),
|
||||
("_repr_png_", "image/png"),
|
||||
("_repr_jpeg_", "image/jpeg"),
|
||||
("_repr_svg_", "image/svg+xml"),
|
||||
("_repr_html_", "text/html"),
|
||||
("_repr_json_", "application/json"),
|
||||
("_repr_javascript_", "application/javascript"),
|
||||
("__repr__", "text/plain"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class HTML:
|
||||
"""
|
||||
Wrap a string so that display() can render it as plain HTML
|
||||
Wrap a string to render as unescaped HTML in `display()`. This is
|
||||
necessary because plain strings are automatically HTML-escaped for safety:
|
||||
|
||||
```python
|
||||
from pyscript import HTML, display
|
||||
|
||||
|
||||
display(HTML("<h1>Hello World</h1>"))
|
||||
```
|
||||
|
||||
Inspired by
|
||||
[`IPython.display.HTML`](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#IPython.display.HTML).
|
||||
"""
|
||||
|
||||
def __init__(self, html):
|
||||
@@ -68,112 +108,156 @@ class HTML:
|
||||
return self._html
|
||||
|
||||
|
||||
def _eval_formatter(obj, print_method):
|
||||
def _get_representation(obj, method):
|
||||
"""
|
||||
Evaluates a formatter method.
|
||||
Call the given representation `method` on an object (`obj`).
|
||||
|
||||
Handles special cases like matplotlib's `savefig`. Returns `None`
|
||||
if the `method` doesn't exist.
|
||||
"""
|
||||
if print_method == "__repr__":
|
||||
if method == "__repr__":
|
||||
return repr(obj)
|
||||
if hasattr(obj, print_method):
|
||||
if print_method == "savefig":
|
||||
buf = io.BytesIO()
|
||||
obj.savefig(buf, format="png")
|
||||
buf.seek(0)
|
||||
return base64.b64encode(buf.read()).decode("utf-8")
|
||||
return getattr(obj, print_method)()
|
||||
if print_method == "_repr_mimebundle_":
|
||||
return {}, {}
|
||||
return None
|
||||
if not hasattr(obj, method):
|
||||
return None
|
||||
if method == "savefig":
|
||||
buf = io.BytesIO()
|
||||
obj.savefig(buf, format="png")
|
||||
buf.seek(0)
|
||||
return base64.b64encode(buf.read()).decode("utf-8")
|
||||
return getattr(obj, method)()
|
||||
|
||||
|
||||
def _format_mime(obj):
|
||||
def _get_content_and_mime(obj):
|
||||
"""
|
||||
Formats object using _repr_x_ methods.
|
||||
Returns the formatted raw content to be inserted into the DOM representing
|
||||
the given object, along with the object's detected MIME type.
|
||||
|
||||
Returns a tuple of (html_string, mime_type).
|
||||
|
||||
Prefers _repr_mimebundle_ if available, otherwise tries individual
|
||||
representation methods, falling back to __repr__ (with a warning in
|
||||
the console).
|
||||
|
||||
Implements a subset of IPython's rich display system (mimebundle support,
|
||||
etc...).
|
||||
"""
|
||||
if isinstance(obj, str):
|
||||
return html.escape(obj), "text/plain"
|
||||
|
||||
mimebundle = _eval_formatter(obj, "_repr_mimebundle_")
|
||||
if isinstance(mimebundle, tuple):
|
||||
format_dict, _ = mimebundle
|
||||
else:
|
||||
format_dict = mimebundle
|
||||
|
||||
output, not_available = None, []
|
||||
for method, mime_type in _MIME_METHODS.items():
|
||||
if mime_type in format_dict:
|
||||
output = format_dict[mime_type]
|
||||
# Prefer an object's mimebundle.
|
||||
mimebundle = _get_representation(obj, "_repr_mimebundle_")
|
||||
if mimebundle:
|
||||
if isinstance(mimebundle, tuple):
|
||||
# Grab global metadata.
|
||||
format_dict, global_meta = mimebundle
|
||||
else:
|
||||
output = _eval_formatter(obj, method)
|
||||
|
||||
if is_none(output):
|
||||
format_dict, global_meta = mimebundle, {}
|
||||
# Try to render using mimebundle formats.
|
||||
for mime_type, output in format_dict.items():
|
||||
if mime_type in _MIME_TO_RENDERERS:
|
||||
meta = global_meta.get(mime_type, {})
|
||||
# If output is a tuple, merge format-specific metadata.
|
||||
if isinstance(output, tuple):
|
||||
output, format_meta = output
|
||||
meta.update(format_meta)
|
||||
return _MIME_TO_RENDERERS[mime_type](output, meta), mime_type
|
||||
# No mimebundle or no available renderers therein, so try individual
|
||||
# methods.
|
||||
for method, mime_type in _METHOD_TO_MIME.items():
|
||||
if mime_type not in _MIME_TO_RENDERERS:
|
||||
continue
|
||||
if mime_type not in _MIME_RENDERERS:
|
||||
not_available.append(mime_type)
|
||||
output = _get_representation(obj, method)
|
||||
if output is None:
|
||||
continue
|
||||
break
|
||||
if is_none(output):
|
||||
if not_available:
|
||||
window.console.warn(
|
||||
f"Rendered object requested unavailable MIME renderers: {not_available}"
|
||||
)
|
||||
output = repr(output)
|
||||
mime_type = "text/plain"
|
||||
elif isinstance(output, tuple):
|
||||
output, meta = output
|
||||
else:
|
||||
meta = {}
|
||||
return _MIME_RENDERERS[mime_type](output, meta), mime_type
|
||||
if isinstance(output, tuple):
|
||||
output, meta = output
|
||||
return _MIME_TO_RENDERERS[mime_type](output, meta), mime_type
|
||||
# Ultimate fallback to repr with warning.
|
||||
window.console.warn(
|
||||
f"Object {type(obj).__name__} has no supported representation method. "
|
||||
"Using __repr__ as fallback."
|
||||
)
|
||||
output = repr(obj)
|
||||
return html.escape(output), "text/plain"
|
||||
|
||||
|
||||
def _write(element, value, append=False):
|
||||
html, mime_type = _format_mime(value)
|
||||
if html == "\\n":
|
||||
def _write_to_dom(element, value, append):
|
||||
"""
|
||||
Given an `element` and a `value`, write formatted content to the referenced
|
||||
DOM element. If `append` is True, content is added to the existing content;
|
||||
otherwise, the existing content is replaced.
|
||||
|
||||
Creates a wrapper `div` when appending multiple items to preserve
|
||||
structure.
|
||||
"""
|
||||
html_content, mime_type = _get_content_and_mime(value)
|
||||
if not html_content.strip():
|
||||
return
|
||||
|
||||
if append:
|
||||
out_element = document.createElement("div")
|
||||
element.append(out_element)
|
||||
container = document.createElement("div")
|
||||
element.append(container)
|
||||
else:
|
||||
out_element = element.lastElementChild
|
||||
if is_none(out_element):
|
||||
out_element = element
|
||||
|
||||
container = element
|
||||
if mime_type in ("application/javascript", "text/html"):
|
||||
script_element = document.createRange().createContextualFragment(html)
|
||||
out_element.append(script_element)
|
||||
container.append(document.createRange().createContextualFragment(html_content))
|
||||
else:
|
||||
out_element.innerHTML = html
|
||||
container.innerHTML = html_content
|
||||
|
||||
|
||||
def display(*values, target=None, append=True):
|
||||
if is_none(target):
|
||||
"""
|
||||
Display Python objects in the web page.
|
||||
|
||||
* `*values`: Python objects to display. Each object is introspected to
|
||||
determine the appropriate rendering method.
|
||||
* `target`: DOM element ID where content should be displayed. If `None`
|
||||
(default), uses the current script tag's designated output area. This
|
||||
can start with '#' (which will be stripped for compatibility).
|
||||
* `append`: If `True` (default), add content to existing output. If
|
||||
`False`, replace existing content before displaying.
|
||||
|
||||
When used in a worker, `display()` requires an explicit `target` parameter
|
||||
to identify where content will be displayed. If used on the main thread,
|
||||
it automatically uses the current `<script>` tag as the target. If the
|
||||
script tag has a `target` attribute, that element will be used instead.
|
||||
|
||||
A ValueError is raised if a valid target cannot be found for the current
|
||||
context.
|
||||
|
||||
```python
|
||||
from pyscript import display, HTML
|
||||
|
||||
|
||||
# Display raw HTML.
|
||||
display(HTML("<h1>Hello, World!</h1>"))
|
||||
|
||||
# Display in current script's output area.
|
||||
display("Hello, World!")
|
||||
|
||||
# Display in a specific element.
|
||||
display("Hello", target="my-div")
|
||||
|
||||
# Replace existing content (note the `#`).
|
||||
display("New content", target="#my-div", append=False)
|
||||
|
||||
# Display multiple values in the default target.
|
||||
display("First", "Second", "Third")
|
||||
```
|
||||
"""
|
||||
if isinstance(target, str):
|
||||
# There's a valid target.
|
||||
target = target[1:] if target.startswith("#") else target
|
||||
elif is_none(target):
|
||||
target = current_target()
|
||||
elif not isinstance(target, str):
|
||||
msg = f"target must be str or None, not {target.__class__.__name__}"
|
||||
raise TypeError(msg)
|
||||
elif target == "":
|
||||
msg = "Cannot have an empty target"
|
||||
raise ValueError(msg)
|
||||
elif target.startswith("#"):
|
||||
# note: here target is str and not None!
|
||||
# align with @when behavior
|
||||
target = target[1:]
|
||||
|
||||
element = document.getElementById(target)
|
||||
|
||||
# If target cannot be found on the page, a ValueError is raised
|
||||
if is_none(element):
|
||||
msg = f"Invalid selector with id={target}. Cannot be found in the page."
|
||||
raise ValueError(msg)
|
||||
|
||||
# if element is a <script type="py">, it has a 'target' attribute which
|
||||
# points to the visual element holding the displayed values. In that case,
|
||||
# use that.
|
||||
raise ValueError(f"Cannot find element with id='{target}' in the page.")
|
||||
# If possible, use a script tag's target attribute.
|
||||
if element.tagName == "SCRIPT" and hasattr(element, "target"):
|
||||
element = element.target
|
||||
|
||||
for v in values:
|
||||
if not append:
|
||||
element.replaceChildren()
|
||||
_write(element, v, append=append)
|
||||
# Clear before displaying all values when not appending.
|
||||
if not append:
|
||||
element.replaceChildren()
|
||||
# Add each value.
|
||||
for value in values:
|
||||
_write_to_dom(element, value, append)
|
||||
|
||||
@@ -1,17 +1,47 @@
|
||||
"""
|
||||
Event handling for PyScript.
|
||||
|
||||
This module provides two complementary systems:
|
||||
|
||||
1. The `Event` class: A simple publish-subscribe pattern for custom events
|
||||
within *your* Python code.
|
||||
|
||||
2. The `@when` decorator: Connects Python functions to browser DOM events,
|
||||
or instances of the `Event` class, allowing you to respond to user
|
||||
interactions like clicks, key presses and form submissions, or to custom
|
||||
events defined in your Python code.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
from functools import wraps
|
||||
from pyscript.magic_js import document
|
||||
from pyscript.ffi import create_proxy
|
||||
from pyscript.context import document
|
||||
from pyscript.ffi import create_proxy, to_js
|
||||
from pyscript.util import is_awaitable
|
||||
from pyscript import config
|
||||
|
||||
|
||||
class Event:
|
||||
"""
|
||||
Represents something that may happen at some point in the future.
|
||||
A custom event that can notify multiple listeners when triggered.
|
||||
|
||||
Use this class to create your own event system within Python code.
|
||||
Listeners can be either regular functions or async functions.
|
||||
|
||||
```python
|
||||
from pyscript.events import Event
|
||||
|
||||
# Create a custom event.
|
||||
data_loaded = Event()
|
||||
|
||||
# Add a listener.
|
||||
def on_data_loaded(result):
|
||||
print(f"Data loaded: {result}")
|
||||
|
||||
data_loaded.add_listener(on_data_loaded)
|
||||
|
||||
# Time passes.... trigger the event.
|
||||
data_loaded.trigger({"data": 123})
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
@@ -19,116 +49,189 @@ class Event:
|
||||
|
||||
def trigger(self, result):
|
||||
"""
|
||||
Trigger the event with a result to pass into the handlers.
|
||||
Trigger the event and notify all listeners with the given `result`.
|
||||
"""
|
||||
for listener in self._listeners:
|
||||
if is_awaitable(listener):
|
||||
# Use create task to avoid making this an async function.
|
||||
asyncio.create_task(listener(result))
|
||||
else:
|
||||
listener(result)
|
||||
|
||||
def add_listener(self, listener):
|
||||
"""
|
||||
Add a callable/awaitable to listen to when this event is triggered.
|
||||
"""
|
||||
if is_awaitable(listener) or callable(listener):
|
||||
if listener not in self._listeners:
|
||||
self._listeners.append(listener)
|
||||
else:
|
||||
msg = "Listener must be callable or awaitable."
|
||||
raise ValueError(msg)
|
||||
Add a function to be called when this event is triggered.
|
||||
|
||||
def remove_listener(self, *args):
|
||||
The `listener` must be callable. It can be either a regular function
|
||||
or an async function. Duplicate listeners are ignored.
|
||||
"""
|
||||
Clear the specified handler functions in *args. If no handlers
|
||||
provided, clear all handlers.
|
||||
if not callable(listener):
|
||||
msg = "Listener must be callable."
|
||||
raise ValueError(msg)
|
||||
if listener not in self._listeners:
|
||||
self._listeners.append(listener)
|
||||
|
||||
def remove_listener(self, *listeners):
|
||||
"""
|
||||
if args:
|
||||
for listener in args:
|
||||
self._listeners.remove(listener)
|
||||
Remove specified `listeners`. If none specified, remove all listeners.
|
||||
"""
|
||||
if listeners:
|
||||
for listener in listeners:
|
||||
try:
|
||||
self._listeners.remove(listener)
|
||||
except ValueError:
|
||||
pass # Silently ignore listeners not in the list.
|
||||
else:
|
||||
self._listeners = []
|
||||
|
||||
|
||||
def when(target, *args, **kwargs):
|
||||
def when(event_type, selector=None, **options):
|
||||
"""
|
||||
Add an event listener to the target element(s) for the specified event type.
|
||||
A decorator to handle DOM events or custom `Event` objects.
|
||||
|
||||
The target can be a string representing the event type, or an Event object.
|
||||
If the target is an Event object, the event listener will be added to that
|
||||
object. If the target is a string, the event listener will be added to the
|
||||
element(s) that match the (second) selector argument.
|
||||
For DOM events, specify the `event_type` (e.g. `"click"`) and a `selector`
|
||||
for target elements. For custom `Event` objects, just pass the `Event`
|
||||
instance as the `event_type`. It's also possible to pass a list of `Event`
|
||||
objects. The `selector` is required only for DOM events. It should be a
|
||||
[CSS selector string](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors),
|
||||
`Element`, `ElementCollection`, or list of DOM elements.
|
||||
|
||||
If a (third) handler argument is provided, it will be called when the event
|
||||
is triggered; thus allowing this to be used as both a function and a
|
||||
decorator.
|
||||
For DOM events only, you can specify optional
|
||||
[addEventListener options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options):
|
||||
`capture`, `once`, `passive`, or `signal`.
|
||||
|
||||
The decorated function can be either a regular function or an async
|
||||
function. If the function accepts an argument, it will receive the event
|
||||
object (for DOM events) or the Event's result (for custom events). A
|
||||
function does not need to accept any arguments if it doesn't require them.
|
||||
|
||||
```python
|
||||
from pyscript import when, display
|
||||
|
||||
# Handle DOM events.
|
||||
@when("click", "#my-button")
|
||||
def handle_click(event):
|
||||
display("Button clicked!")
|
||||
|
||||
# Handle DOM events with options.
|
||||
@when("click", "#my-button", once=True)
|
||||
def handle_click_once(event):
|
||||
display("Button clicked once!")
|
||||
|
||||
# Handle custom events.
|
||||
my_event = Event()
|
||||
|
||||
@when(my_event)
|
||||
def handle_custom(): # No event argument needed.
|
||||
display("Custom event triggered!")
|
||||
|
||||
# Handle multiple custom events.
|
||||
another_event = Event()
|
||||
|
||||
def another_handler():
|
||||
display("Another custom event handler.")
|
||||
|
||||
# Attach the same handler to multiple events but not as a decorator.
|
||||
when([my_event, another_event])(another_handler)
|
||||
|
||||
# Trigger an Event instance from a DOM event via @when.
|
||||
@when("click", "#my-button")
|
||||
def handle_click(event):
|
||||
another_event.trigger("Button clicked!")
|
||||
|
||||
# Stacked decorators also work.
|
||||
@when("mouseover", "#my-div")
|
||||
@when(my_event)
|
||||
def handle_both(event):
|
||||
display("Either mouseover or custom event triggered!")
|
||||
```
|
||||
"""
|
||||
# If "when" is called as a function, try to grab the handler from the
|
||||
# arguments. If there's no handler, this must be a decorator based call.
|
||||
handler = None
|
||||
if args and (callable(args[0]) or is_awaitable(args[0])):
|
||||
handler = args[0]
|
||||
elif callable(kwargs.get("handler")) or is_awaitable(kwargs.get("handler")):
|
||||
handler = kwargs.pop("handler")
|
||||
# If the target is a string, it is the "older" use of `when` where it
|
||||
# represents the name of a DOM event.
|
||||
if isinstance(target, str):
|
||||
# Extract the selector from the arguments or keyword arguments.
|
||||
selector = args[0] if args else kwargs.pop("selector")
|
||||
if isinstance(event_type, str):
|
||||
# This is a DOM event to handle, so check and use the selector.
|
||||
if not selector:
|
||||
msg = "No selector provided."
|
||||
raise ValueError(msg)
|
||||
# Grab the DOM elements to which the target event will be attached.
|
||||
from pyscript.web import Element, ElementCollection
|
||||
|
||||
if isinstance(selector, str):
|
||||
elements = document.querySelectorAll(selector)
|
||||
elif isinstance(selector, Element):
|
||||
elements = [selector._dom_element]
|
||||
elif isinstance(selector, ElementCollection):
|
||||
elements = [el._dom_element for el in selector]
|
||||
else:
|
||||
elements = selector if isinstance(selector, list) else [selector]
|
||||
raise ValueError("Selector required for DOM event handling.")
|
||||
elements = _get_elements(selector)
|
||||
if not elements:
|
||||
raise ValueError(f"No elements found for selector: {selector}")
|
||||
|
||||
def decorator(func):
|
||||
sig = inspect.signature(func)
|
||||
if sig.parameters:
|
||||
if is_awaitable(func):
|
||||
|
||||
async def wrapper(event):
|
||||
return await func(event)
|
||||
|
||||
else:
|
||||
wrapper = func
|
||||
wrapper = _create_wrapper(func)
|
||||
if isinstance(event_type, Event):
|
||||
# Custom Event - add listener.
|
||||
event_type.add_listener(wrapper)
|
||||
elif isinstance(event_type, list) and all(
|
||||
isinstance(t, Event) for t in event_type
|
||||
):
|
||||
# List of custom Events - add listener to each.
|
||||
for event in event_type:
|
||||
event.add_listener(wrapper)
|
||||
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)
|
||||
if isinstance(target, Event):
|
||||
# The target is a single Event object.
|
||||
target.add_listener(wrapper)
|
||||
elif isinstance(target, list) and all(isinstance(t, Event) for t in target):
|
||||
# The target is a list of Event objects.
|
||||
for evt in target:
|
||||
evt.add_listener(wrapper)
|
||||
else:
|
||||
# The target is a string representing an event type, and so a
|
||||
# DOM element or collection of elements is found in "elements".
|
||||
for el in elements:
|
||||
el.addEventListener(target, create_proxy(wrapper))
|
||||
# DOM event - attach to all matched elements.
|
||||
for element in elements:
|
||||
element.addEventListener(
|
||||
event_type,
|
||||
create_proxy(wrapper),
|
||||
to_js(options) if options else False,
|
||||
)
|
||||
return wrapper
|
||||
|
||||
# If "when" was called as a decorator, return the decorator function,
|
||||
# otherwise just call the internal decorator function with the supplied
|
||||
# handler.
|
||||
return decorator(handler) if handler else decorator
|
||||
return decorator
|
||||
|
||||
|
||||
def _get_elements(selector):
|
||||
"""
|
||||
Convert various `selector` types into a list of DOM elements.
|
||||
"""
|
||||
from pyscript.web import Element, ElementCollection
|
||||
|
||||
if isinstance(selector, str):
|
||||
return list(document.querySelectorAll(selector))
|
||||
elif isinstance(selector, Element):
|
||||
return [selector._dom_element]
|
||||
elif isinstance(selector, ElementCollection):
|
||||
return [el._dom_element for el in selector]
|
||||
elif isinstance(selector, list):
|
||||
return selector
|
||||
else:
|
||||
return [selector]
|
||||
|
||||
|
||||
def _create_wrapper(func):
|
||||
"""
|
||||
Create an appropriate wrapper for the given function, `func`.
|
||||
|
||||
The wrapper handles both sync and async functions, and respects whether
|
||||
the function expects to receive event arguments.
|
||||
"""
|
||||
# Get the original function if it's been wrapped. This avoids wrapper
|
||||
# loops when stacking decorators.
|
||||
original_func = func
|
||||
while hasattr(original_func, "__wrapped__"):
|
||||
original_func = original_func.__wrapped__
|
||||
# Inspect the original function signature.
|
||||
sig = inspect.signature(original_func)
|
||||
accepts_args = bool(sig.parameters)
|
||||
if is_awaitable(func):
|
||||
if accepts_args:
|
||||
|
||||
async def wrapper(event):
|
||||
return await func(event)
|
||||
|
||||
else:
|
||||
|
||||
async def wrapper(*args, **kwargs):
|
||||
return await func()
|
||||
|
||||
else:
|
||||
if accepts_args:
|
||||
# Always create a new wrapper function to avoid issues with
|
||||
# stacked decorators getting into an infinite loop.
|
||||
|
||||
def wrapper(event):
|
||||
return func(event)
|
||||
|
||||
else:
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
return func()
|
||||
|
||||
return wraps(func)(wrapper)
|
||||
|
||||
@@ -1,87 +1,218 @@
|
||||
import json
|
||||
"""
|
||||
This module provides a Python-friendly interface to the
|
||||
[browser's fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API),
|
||||
returning native Python data types and supporting directly awaiting the promise
|
||||
and chaining method calls directly on the promise.
|
||||
|
||||
```python
|
||||
from pyscript.fetch import fetch
|
||||
url = "https://api.example.com/data"
|
||||
|
||||
# Pattern 1: Await the response, then extract data.
|
||||
response = await fetch(url)
|
||||
if response.ok:
|
||||
data = await response.json()
|
||||
else:
|
||||
raise NetworkError(f"Fetch failed: {response.status}")
|
||||
|
||||
# Pattern 2: Chain method calls directly on the promise.
|
||||
data = await fetch(url).json()
|
||||
```
|
||||
"""
|
||||
|
||||
import json
|
||||
import js
|
||||
from pyscript.util import as_bytearray
|
||||
|
||||
|
||||
### wrap the response to grant Pythonic results
|
||||
class _Response:
|
||||
class _FetchResponse:
|
||||
"""
|
||||
Wraps a JavaScript Response object with Pythonic data extraction methods.
|
||||
|
||||
This wrapper ensures that data returned from fetch is, if possible, in
|
||||
native Python types rather than JavaScript types.
|
||||
"""
|
||||
|
||||
def __init__(self, response):
|
||||
self._response = response
|
||||
|
||||
# grant access to response.ok and other fields
|
||||
def __getattr__(self, attr):
|
||||
"""
|
||||
Provide access to underlying Response properties like ok, status, etc.
|
||||
"""
|
||||
return getattr(self._response, attr)
|
||||
|
||||
# exposed methods with Pythonic results
|
||||
async def arrayBuffer(self):
|
||||
"""
|
||||
Get response body as a buffer (memoryview or bytes).
|
||||
|
||||
Returns a memoryview in MicroPython or bytes in Pyodide, representing
|
||||
the raw binary data.
|
||||
"""
|
||||
buffer = await self._response.arrayBuffer()
|
||||
# works in Pyodide
|
||||
if hasattr(buffer, "to_py"):
|
||||
# Pyodide conversion.
|
||||
return buffer.to_py()
|
||||
# shims in MicroPython
|
||||
# MicroPython conversion.
|
||||
return memoryview(as_bytearray(buffer))
|
||||
|
||||
async def blob(self):
|
||||
"""
|
||||
Get response body as a JavaScript Blob object.
|
||||
|
||||
Returns the raw JS Blob for use with other JS APIs.
|
||||
"""
|
||||
return await self._response.blob()
|
||||
|
||||
async def bytearray(self):
|
||||
"""
|
||||
Get response body as a Python bytearray.
|
||||
|
||||
Returns a mutable bytearray containing the response data.
|
||||
"""
|
||||
buffer = await self._response.arrayBuffer()
|
||||
return as_bytearray(buffer)
|
||||
|
||||
async def json(self):
|
||||
"""
|
||||
Parse response body as JSON and return Python objects.
|
||||
|
||||
Returns native Python dicts, lists, strings, numbers, etc.
|
||||
"""
|
||||
return json.loads(await self.text())
|
||||
|
||||
async def text(self):
|
||||
"""
|
||||
Get response body as a text string.
|
||||
"""
|
||||
return await self._response.text()
|
||||
|
||||
|
||||
### allow direct await to _Response methods
|
||||
class _DirectResponse:
|
||||
@staticmethod
|
||||
def setup(promise, response):
|
||||
promise._response = _Response(response)
|
||||
return promise._response
|
||||
class _FetchPromise:
|
||||
"""
|
||||
Wraps the fetch promise to enable direct method chaining.
|
||||
|
||||
This allows calling response methods directly on the fetch promise:
|
||||
`await fetch(url).json()` instead of requiring two separate awaits.
|
||||
|
||||
This feels more Pythonic since it matches typical usage patterns
|
||||
Python developers have got used to via libraries like `requests`.
|
||||
"""
|
||||
|
||||
def __init__(self, promise):
|
||||
self._promise = promise
|
||||
# To be resolved in the future via the setup() static method.
|
||||
promise._response = None
|
||||
# Add convenience methods directly to the promise.
|
||||
promise.arrayBuffer = self.arrayBuffer
|
||||
promise.blob = self.blob
|
||||
promise.bytearray = self.bytearray
|
||||
promise.json = self.json
|
||||
promise.text = self.text
|
||||
|
||||
async def _response(self):
|
||||
@staticmethod
|
||||
def setup(promise, response):
|
||||
"""
|
||||
Store the resolved response on the promise for later access.
|
||||
"""
|
||||
promise._response = _FetchResponse(response)
|
||||
return promise._response
|
||||
|
||||
async def _get_response(self):
|
||||
"""
|
||||
Get the cached response, or await the promise if not yet resolved.
|
||||
"""
|
||||
if not self._promise._response:
|
||||
await self._promise
|
||||
return self._promise._response
|
||||
|
||||
async def arrayBuffer(self):
|
||||
response = await self._response()
|
||||
response = await self._get_response()
|
||||
return await response.arrayBuffer()
|
||||
|
||||
async def blob(self):
|
||||
response = await self._response()
|
||||
response = await self._get_response()
|
||||
return await response.blob()
|
||||
|
||||
async def bytearray(self):
|
||||
response = await self._response()
|
||||
response = await self._get_response()
|
||||
return await response.bytearray()
|
||||
|
||||
async def json(self):
|
||||
response = await self._response()
|
||||
response = await self._get_response()
|
||||
return await response.json()
|
||||
|
||||
async def text(self):
|
||||
response = await self._response()
|
||||
response = await self._get_response()
|
||||
return await response.text()
|
||||
|
||||
|
||||
def fetch(url, **kw):
|
||||
# workaround Pyodide / MicroPython dict <-> js conversion
|
||||
options = js.JSON.parse(json.dumps(kw))
|
||||
awaited = lambda response, *args: _DirectResponse.setup(promise, response)
|
||||
promise = js.fetch(url, options).then(awaited)
|
||||
_DirectResponse(promise)
|
||||
def fetch(url, **options):
|
||||
"""
|
||||
Fetch a resource from the network using a Pythonic interface.
|
||||
|
||||
This wraps JavaScript's fetch API, returning Python-native data types
|
||||
and supporting both direct promise awaiting and method chaining.
|
||||
|
||||
The function takes a `url` and optional fetch `options` as keyword
|
||||
arguments. The `options` correspond to the JavaScript fetch API's
|
||||
[RequestInit dictionary](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit),
|
||||
and commonly include:
|
||||
|
||||
- `method`: HTTP method (e.g., `"GET"`, `"POST"`, `"PUT"` etc.)
|
||||
- `headers`: Dict of request headers.
|
||||
- `body`: Request body (string, dict for JSON, etc.)
|
||||
|
||||
The function returns a promise that resolves to a Response-like object
|
||||
with Pythonic methods to extract data:
|
||||
|
||||
- `await response.json()` to get JSON as Python objects.
|
||||
- `await response.text()` to get text data.
|
||||
- `await response.bytearray()` to get raw data as a bytearray.
|
||||
- `await response.arrayBuffer()` to get raw data as a memoryview or bytes.
|
||||
- `await response.blob()` to get the raw JS Blob object.
|
||||
|
||||
It's also possible to chain these methods directly on the fetch promise:
|
||||
`data = await fetch(url).json()`
|
||||
|
||||
The returned response object also exposes standard properties like
|
||||
`ok`, `status`, and `statusText` for checking response status.
|
||||
|
||||
```python
|
||||
# Simple GET request.
|
||||
response = await fetch("https://api.example.com/data")
|
||||
data = await response.json()
|
||||
|
||||
# Method chaining.
|
||||
data = await fetch("https://api.example.com/data").json()
|
||||
|
||||
# POST request with JSON.
|
||||
response = await fetch(
|
||||
"https://api.example.com/users",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
body=json.dumps({"name": "Alice"})
|
||||
)
|
||||
result = await response.json()
|
||||
|
||||
# Check response status codes.
|
||||
response = await fetch("https://api.example.com/data")
|
||||
if response.ok:
|
||||
# Status in the range 200-299.
|
||||
data = await response.json()
|
||||
elif response.status == 404:
|
||||
print("Resource not found")
|
||||
else:
|
||||
print(f"Error: {response.status} {response.statusText}")
|
||||
```
|
||||
"""
|
||||
# Convert Python dict to JavaScript object.
|
||||
js_options = js.JSON.parse(json.dumps(options))
|
||||
|
||||
# Setup response handler to wrap the result.
|
||||
def on_response(response, *_):
|
||||
return _FetchPromise.setup(promise, response)
|
||||
|
||||
promise = js.fetch(url, js_options).then(on_response)
|
||||
_FetchPromise(promise)
|
||||
return promise
|
||||
|
||||
@@ -1,48 +1,164 @@
|
||||
"""
|
||||
This module provides a unified
|
||||
[Foreign Function Interface (FFI)](https://en.wikipedia.org/wiki/Foreign_function_interface)
|
||||
layer for Python/JavaScript interactions, that works consistently across both
|
||||
Pyodide and MicroPython, and in a worker or main thread context, abstracting
|
||||
away the differences in their JavaScript interop APIs.
|
||||
|
||||
The following utilities work on both the main thread and in worker contexts:
|
||||
|
||||
- `create_proxy`: Create a persistent JavaScript proxy of a Python function.
|
||||
- `to_js`: Convert Python objects to JavaScript objects.
|
||||
- `is_none`: Check if a value is Python `None` or JavaScript `null`.
|
||||
- `assign`: Merge objects (like JavaScript's `Object.assign`).
|
||||
|
||||
The following utilities are specific to worker contexts:
|
||||
|
||||
- `direct`: Mark objects for direct JavaScript access.
|
||||
- `gather`: Collect multiple values from worker contexts.
|
||||
- `query`: Query objects in worker contexts.
|
||||
|
||||
More details of the `direct`, `gather`, and `query` utilities
|
||||
[can be found here](https://github.com/WebReflection/reflected-ffi?tab=readme-ov-file#remote-extra-utilities).
|
||||
"""
|
||||
|
||||
try:
|
||||
# Attempt to import Pyodide's FFI utilities.
|
||||
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"):
|
||||
def _to_js_wrapper(value, **kw):
|
||||
if "dict_converter" not in kw:
|
||||
kw["dict_converter"] = from_entries
|
||||
return _py_tjs(value, **kw)
|
||||
|
||||
except:
|
||||
# Fallback to jsffi for MicroPython.
|
||||
from jsffi import create_proxy as _cp
|
||||
from jsffi import to_js as _tjs
|
||||
from jsffi import to_js as _to_js_wrapper
|
||||
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
|
||||
|
||||
def create_proxy(func):
|
||||
"""
|
||||
Create a persistent JavaScript proxy of a Python function.
|
||||
|
||||
This proxy allows JavaScript code to call the Python function
|
||||
seamlessly, maintaining the correct context and argument handling.
|
||||
|
||||
This is especially useful when passing Python functions as callbacks
|
||||
to JavaScript APIs (without `create_proxy`, the function would be
|
||||
garbage collected after the declaration of the callback).
|
||||
|
||||
```python
|
||||
from pyscript import ffi
|
||||
from pyscript import document
|
||||
|
||||
my_button = document.getElementById("my-button")
|
||||
|
||||
def py_callback(x):
|
||||
print(f"Callback called with {x}")
|
||||
|
||||
my_button.addEventListener("click", ffi.create_proxy(py_callback))
|
||||
```
|
||||
"""
|
||||
return _cp(func)
|
||||
|
||||
|
||||
def to_js(value, **kw):
|
||||
"""
|
||||
Convert Python objects to JavaScript objects.
|
||||
|
||||
This ensures a Python `dict` becomes a
|
||||
[proper JavaScript object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)
|
||||
rather a JavaScript [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map),
|
||||
which is more intuitive for most use cases.
|
||||
|
||||
Where required, the underlying `to_js` uses `Object.fromEntries` for
|
||||
`dict` conversion.
|
||||
|
||||
```python
|
||||
from pyscript import ffi
|
||||
import js
|
||||
|
||||
|
||||
note = {
|
||||
"body": "This is a notification",
|
||||
"icon": "icon.png"
|
||||
}
|
||||
|
||||
js.Notification.new("Hello!", ffi.to_js(note))
|
||||
```
|
||||
"""
|
||||
return _to_js_wrapper(value, **kw)
|
||||
|
||||
|
||||
def is_none(value):
|
||||
"""
|
||||
Check if a value is `None` or JavaScript `null`.
|
||||
|
||||
In Pyodide, JavaScript `null` is represented by the `jsnull` object,
|
||||
so we check for both Python `None` and `jsnull`. This function ensures
|
||||
consistent behavior across Pyodide and MicroPython for null-like
|
||||
values.
|
||||
|
||||
```python
|
||||
from pyscript import ffi
|
||||
import js
|
||||
|
||||
|
||||
val1 = None
|
||||
val2 = js.null
|
||||
val3 = 42
|
||||
|
||||
print(ffi.is_none(val1)) # True
|
||||
print(ffi.is_none(val2)) # True
|
||||
print(ffi.is_none(val3)) # False
|
||||
```
|
||||
"""
|
||||
return value is None or value is jsnull
|
||||
|
||||
|
||||
try:
|
||||
# Worker context utilities from reflected-ffi.
|
||||
# See https://github.com/WebReflection/reflected-ffi for more details.
|
||||
from polyscript import ffi as _ffi
|
||||
|
||||
_assign = _ffi.assign
|
||||
|
||||
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:
|
||||
# Fallback implementations for main thread context.
|
||||
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
|
||||
|
||||
def assign(source, *args):
|
||||
"""
|
||||
Merge JavaScript objects (like
|
||||
[Object.assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)).
|
||||
|
||||
Takes a target object and merges properties from one or more source
|
||||
objects into it, returning the modified target.
|
||||
|
||||
```python
|
||||
obj = js.Object.new()
|
||||
ffi.assign(obj, {"a": 1}, {"b": 2})
|
||||
# obj now has properties a=1 and b=2
|
||||
```
|
||||
"""
|
||||
for arg in args:
|
||||
_assign(source, to_js(arg))
|
||||
return source
|
||||
|
||||
@@ -1,4 +1,36 @@
|
||||
# https://www.npmjs.com/package/flatted
|
||||
"""
|
||||
This module is a Python implementation of the
|
||||
[Flatted JavaScript library](https://www.npmjs.com/package/flatted), which
|
||||
provides a light and fast way to serialize and deserialize JSON structures
|
||||
that contain circular references.
|
||||
|
||||
Standard JSON cannot handle circular references - attempting to serialize an
|
||||
object that references itself will cause an error. Flatted solves this by
|
||||
transforming circular structures into a flat array format that can be safely
|
||||
serialized and later reconstructed.
|
||||
|
||||
Common use cases:
|
||||
|
||||
- Serializing complex object graphs with circular references.
|
||||
- Working with DOM-like structures that contain parent/child references.
|
||||
- Preserving object identity when serializing data structures.
|
||||
|
||||
```python
|
||||
from pyscript import flatted
|
||||
|
||||
|
||||
# Create a circular structure.
|
||||
obj = {"name": "parent"}
|
||||
obj["self"] = obj # Circular reference!
|
||||
|
||||
# Standard json.dumps would fail here.
|
||||
serialized = flatted.stringify(obj)
|
||||
|
||||
# Reconstruct the original structure.
|
||||
restored = flatted.parse(serialized)
|
||||
assert restored["self"] is restored # Circular reference preserved!
|
||||
```
|
||||
"""
|
||||
|
||||
import json as _json
|
||||
|
||||
@@ -114,6 +146,26 @@ def _wrap(value):
|
||||
|
||||
|
||||
def parse(value, *args, **kwargs):
|
||||
"""
|
||||
Parse a Flatted JSON string and reconstruct the original structure.
|
||||
|
||||
This function takes a `value` containing a JSON string created by
|
||||
Flatted's stringify() and reconstructs the original Python object,
|
||||
including any circular references. The `*args` and `**kwargs` are passed
|
||||
to json.loads() for additional customization.
|
||||
|
||||
```python
|
||||
from pyscript import flatted
|
||||
|
||||
|
||||
# Parse a Flatted JSON string.
|
||||
json_string = '[{"name": "1", "self": "0"}, "parent"]'
|
||||
obj = flatted.parse(json_string)
|
||||
|
||||
# Circular references are preserved.
|
||||
assert obj["self"] is obj
|
||||
```
|
||||
"""
|
||||
json = _json.loads(value, *args, **kwargs)
|
||||
wrapped = []
|
||||
for value in json:
|
||||
@@ -138,6 +190,31 @@ def parse(value, *args, **kwargs):
|
||||
|
||||
|
||||
def stringify(value, *args, **kwargs):
|
||||
"""
|
||||
Serialize a Python object to a Flatted JSON string.
|
||||
|
||||
This function converts `value`, a Python object (including those with
|
||||
circular references), into a JSON string that can be safely transmitted
|
||||
or stored. The resulting string can be reconstructed using Flatted's
|
||||
parse(). The `*args` and `**kwargs` are passed to json.dumps() for
|
||||
additional customization.
|
||||
|
||||
```python
|
||||
from pyscript import flatted
|
||||
|
||||
|
||||
# Create an object with a circular reference.
|
||||
parent = {"name": "parent", "children": []}
|
||||
child = {"name": "child", "parent": parent}
|
||||
parent["children"].append(child)
|
||||
|
||||
# Serialize it (standard json.dumps would fail here).
|
||||
json_string = flatted.stringify(parent)
|
||||
|
||||
# Can optionally pretty-print via JSON indentation etc.
|
||||
pretty = flatted.stringify(parent, indent=2)
|
||||
```
|
||||
"""
|
||||
known = _Known()
|
||||
input = []
|
||||
output = []
|
||||
|
||||
@@ -1,7 +1,63 @@
|
||||
"""
|
||||
This module provides an API for mounting directories from the user's local
|
||||
filesystem into the browser's virtual filesystem. This means Python code,
|
||||
running in the browser, can read and write files on the user's local machine.
|
||||
|
||||
!!! warning
|
||||
**This API only works in Chromium-based browsers** (Chrome, Edge,
|
||||
Vivaldi, Brave, etc.) that support the
|
||||
[File System Access API](https://wicg.github.io/file-system-access/).
|
||||
|
||||
The module maintains a `mounted` dictionary that tracks all currently mounted
|
||||
paths and their associated filesystem handles.
|
||||
|
||||
```python
|
||||
from pyscript import fs, document, when
|
||||
|
||||
|
||||
# Mount a local directory to the `/local` mount point in the browser's
|
||||
# virtual filesystem (may prompt user for permission).
|
||||
await fs.mount("/local")
|
||||
|
||||
# Alternatively, mount on a button click event. This is important because
|
||||
# if the call to `fs.mount` happens after a click or other transient event,
|
||||
# the confirmation dialog will not be shown.
|
||||
@when("click", "#mount-button")
|
||||
async def handler(event):
|
||||
await fs.mount("/another_dir")
|
||||
|
||||
# Work with files in the mounted directory as usual.
|
||||
with open("/local/example.txt", "w") as f:
|
||||
f.write("Hello from PyScript!")
|
||||
|
||||
# Ensure changes are written to local filesystem.
|
||||
await fs.sync("/local")
|
||||
|
||||
# Clean up when done.
|
||||
await fs.unmount("/local")
|
||||
```
|
||||
"""
|
||||
|
||||
import js
|
||||
from _pyscript import fs as _fs, interpreter
|
||||
from pyscript import window
|
||||
from pyscript.ffi import to_js
|
||||
from pyscript.context import RUNNING_IN_WORKER
|
||||
|
||||
# Worker-specific imports.
|
||||
if RUNNING_IN_WORKER:
|
||||
from pyscript.context import sync as sync_with_worker
|
||||
from polyscript import IDBMap
|
||||
|
||||
mounted = {}
|
||||
"""Global dictionary tracking mounted paths and their filesystem handles."""
|
||||
|
||||
|
||||
async def get_handler(details):
|
||||
async def _check_permission(details):
|
||||
"""
|
||||
Check if permission has been granted for a filesystem handler. Returns
|
||||
the handler if permission is granted, otherwise None.
|
||||
"""
|
||||
handler = details.handler
|
||||
options = details.options
|
||||
permission = await handler.queryPermission(options)
|
||||
@@ -9,94 +65,194 @@ async def get_handler(details):
|
||||
|
||||
|
||||
async def mount(path, mode="readwrite", root="", id="pyscript"):
|
||||
import js
|
||||
from _pyscript import fs, interpreter
|
||||
from pyscript.ffi import to_js
|
||||
from pyscript.magic_js import (
|
||||
RUNNING_IN_WORKER,
|
||||
sync,
|
||||
)
|
||||
"""
|
||||
Mount a directory from the local filesystem to the virtual filesystem
|
||||
at the specified `path` mount point. The `mode` can be "readwrite" or
|
||||
"read" to specify access level. The `root` parameter provides a hint
|
||||
for the file picker starting location. The `id` parameter allows multiple
|
||||
distinct mounts at the same path.
|
||||
|
||||
On first use, the browser will prompt the user to select a directory
|
||||
and grant permission.
|
||||
|
||||
```python
|
||||
from pyscript import fs
|
||||
|
||||
|
||||
# Basic mount with default settings.
|
||||
await fs.mount("/local")
|
||||
|
||||
# Mount with read-only access.
|
||||
await fs.mount("/readonly", mode="read")
|
||||
|
||||
# Mount with a hint to start in Downloads folder.
|
||||
await fs.mount("/downloads", root="downloads")
|
||||
|
||||
# Mount with a custom ID to track different directories.
|
||||
await fs.mount("/project", id="my-project")
|
||||
```
|
||||
|
||||
If called during a user interaction (like a button click), the
|
||||
permission dialog may be skipped if permission was previously granted.
|
||||
"""
|
||||
js.console.warn("experimental pyscript.fs ⚠️")
|
||||
|
||||
# Check if path is already mounted with a different ID.
|
||||
mount_key = f"{path}@{id}"
|
||||
if path in mounted:
|
||||
# Path already mounted - check if it's the same ID.
|
||||
for existing_key in mounted.keys():
|
||||
if existing_key.startswith(f"{path}@") and existing_key != mount_key:
|
||||
raise ValueError(
|
||||
f"Path '{path}' is already mounted with a different ID. "
|
||||
f"Unmount it first or use a different path."
|
||||
)
|
||||
|
||||
details = None
|
||||
handler = None
|
||||
|
||||
uid = f"{path}@{id}"
|
||||
|
||||
options = {"id": id, "mode": mode}
|
||||
if root != "":
|
||||
options["startIn"] = root
|
||||
|
||||
if RUNNING_IN_WORKER:
|
||||
fsh = sync.storeFSHandler(uid, to_js(options))
|
||||
fs_handler = sync_with_worker.storeFSHandler(mount_key, to_js(options))
|
||||
|
||||
# allow both async and/or SharedArrayBuffer use case
|
||||
if isinstance(fsh, bool):
|
||||
success = fsh
|
||||
# Handle both async and SharedArrayBuffer use cases.
|
||||
if isinstance(fs_handler, bool):
|
||||
success = fs_handler
|
||||
else:
|
||||
success = await fsh
|
||||
success = await fs_handler
|
||||
|
||||
if success:
|
||||
from polyscript import IDBMap
|
||||
from pyscript import window
|
||||
|
||||
idbm = IDBMap.new(fs.NAMESPACE)
|
||||
details = await idbm.get(uid)
|
||||
handler = await get_handler(details)
|
||||
idbm = IDBMap.new(_fs.NAMESPACE)
|
||||
details = await idbm.get(mount_key)
|
||||
handler = await _check_permission(details)
|
||||
if handler is None:
|
||||
# force await in either async or sync scenario
|
||||
await js.Promise.resolve(sync.getFSHandler(details.options))
|
||||
# Force await in either async or sync scenario.
|
||||
await js.Promise.resolve(sync_with_worker.getFSHandler(details.options))
|
||||
handler = details.handler
|
||||
|
||||
else:
|
||||
raise RuntimeError(fs.ERROR)
|
||||
raise RuntimeError(_fs.ERROR)
|
||||
|
||||
else:
|
||||
success = await fs.idb.has(uid)
|
||||
success = await _fs.idb.has(mount_key)
|
||||
|
||||
if success:
|
||||
details = await fs.idb.get(uid)
|
||||
handler = await get_handler(details)
|
||||
details = await _fs.idb.get(mount_key)
|
||||
handler = await _check_permission(details)
|
||||
if handler is None:
|
||||
handler = await fs.getFileSystemDirectoryHandle(details.options)
|
||||
handler = await _fs.getFileSystemDirectoryHandle(details.options)
|
||||
else:
|
||||
js_options = to_js(options)
|
||||
handler = await fs.getFileSystemDirectoryHandle(js_options)
|
||||
handler = await _fs.getFileSystemDirectoryHandle(js_options)
|
||||
details = {"handler": handler, "options": js_options}
|
||||
await fs.idb.set(uid, to_js(details))
|
||||
await _fs.idb.set(mount_key, to_js(details))
|
||||
|
||||
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):
|
||||
"""
|
||||
Synchronise the virtual and local filesystems for a mounted `path`.
|
||||
|
||||
This ensures all changes made in the browser's virtual filesystem are
|
||||
written to the user's local filesystem, and vice versa.
|
||||
|
||||
```python
|
||||
from pyscript import fs
|
||||
|
||||
|
||||
await fs.mount("/local")
|
||||
|
||||
# Make changes to files.
|
||||
with open("/local/data.txt", "w") as f:
|
||||
f.write("Important data")
|
||||
|
||||
# Ensure changes are written to local disk.
|
||||
await fs.sync("/local")
|
||||
```
|
||||
|
||||
This is automatically called by unmount(), but you may want to call
|
||||
it explicitly to ensure data persistence at specific points.
|
||||
"""
|
||||
if path not in mounted:
|
||||
raise KeyError(
|
||||
f"Path '{path}' is not mounted. " f"Use fs.mount() to mount it first."
|
||||
)
|
||||
await mounted[path].syncfs()
|
||||
|
||||
|
||||
async def unmount(path):
|
||||
from _pyscript import interpreter
|
||||
"""
|
||||
Unmount a directory, specified by `path`, from the virtual filesystem.
|
||||
|
||||
This synchronises any pending changes and then removes the mount point,
|
||||
freeing up memory. The `path` can be reused for mounting a different
|
||||
directory.
|
||||
|
||||
```python
|
||||
from pyscript import fs
|
||||
|
||||
|
||||
await fs.mount("/local")
|
||||
# ... work with files ...
|
||||
await fs.unmount("/local")
|
||||
|
||||
# Path can now be reused.
|
||||
await fs.mount("/local", id="different-folder")
|
||||
```
|
||||
|
||||
This automatically calls `sync()` before unmounting to ensure no data
|
||||
is lost.
|
||||
"""
|
||||
if path not in mounted:
|
||||
raise KeyError(f"Path '{path}' is not mounted. Cannot unmount.")
|
||||
|
||||
await sync(path)
|
||||
interpreter._module.FS.unmount(path)
|
||||
del mounted[path]
|
||||
|
||||
|
||||
async def revoke(path, id="pyscript"):
|
||||
"""
|
||||
Revoke filesystem access permission and unmount for a given
|
||||
`path` and `id` combination.
|
||||
|
||||
This removes the stored permission for accessing the user's local
|
||||
filesystem at the specified path and ID. Unlike `unmount()`, which only
|
||||
removes the mount point, `revoke()` also clears the permission so the
|
||||
user will be prompted again on next mount.
|
||||
|
||||
```python
|
||||
from pyscript import fs
|
||||
|
||||
|
||||
await fs.mount("/local", id="my-app")
|
||||
# ... work with files ...
|
||||
|
||||
# Revoke permission (user will be prompted again next time).
|
||||
revoked = await fs.revoke("/local", id="my-app")
|
||||
|
||||
if revoked:
|
||||
print("Permission revoked successfully")
|
||||
```
|
||||
|
||||
After revoking, the user will need to grant permission again and
|
||||
select a directory when `mount()` is called next time.
|
||||
"""
|
||||
mount_key = f"{path}@{id}"
|
||||
|
||||
if RUNNING_IN_WORKER:
|
||||
handler_exists = sync_with_worker.deleteFSHandler(mount_key)
|
||||
else:
|
||||
handler_exists = await _fs.idb.has(mount_key)
|
||||
if handler_exists:
|
||||
handler_exists = await _fs.idb.delete(mount_key)
|
||||
|
||||
if handler_exists:
|
||||
interpreter._module.FS.unmount(path)
|
||||
if path in mounted:
|
||||
del mounted[path]
|
||||
|
||||
return handler_exists
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import json
|
||||
import sys
|
||||
|
||||
import js as globalThis
|
||||
from polyscript import config as _config
|
||||
from polyscript import js_modules
|
||||
from pyscript.util import NotSupported
|
||||
|
||||
RUNNING_IN_WORKER = not hasattr(globalThis, "document")
|
||||
|
||||
config = json.loads(globalThis.JSON.stringify(_config))
|
||||
|
||||
if isinstance(config, str):
|
||||
config = {}
|
||||
|
||||
if "MicroPython" in sys.version:
|
||||
config["type"] = "mpy"
|
||||
else:
|
||||
config["type"] = "py"
|
||||
|
||||
|
||||
# allow `from pyscript.js_modules.xxx import yyy`
|
||||
class JSModule:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __getattr__(self, field):
|
||||
# avoid pyodide looking for non existent fields
|
||||
if not field.startswith("_"):
|
||||
return getattr(getattr(js_modules, self.name), field)
|
||||
return None
|
||||
|
||||
|
||||
# generate N modules in the system that will proxy the real value
|
||||
for name in globalThis.Reflect.ownKeys(js_modules):
|
||||
sys.modules[f"pyscript.js_modules.{name}"] = JSModule(name)
|
||||
sys.modules["pyscript.js_modules"] = js_modules
|
||||
|
||||
if RUNNING_IN_WORKER:
|
||||
import polyscript
|
||||
|
||||
PyWorker = NotSupported(
|
||||
"pyscript.PyWorker",
|
||||
"pyscript.PyWorker works only when running in the main thread",
|
||||
)
|
||||
|
||||
try:
|
||||
import js
|
||||
|
||||
window = polyscript.xworker.window
|
||||
document = window.document
|
||||
js.document = document
|
||||
# this is the same as js_import on main and it lands modules on main
|
||||
js_import = window.Function(
|
||||
"return (...urls) => Promise.all(urls.map((url) => import(url)))"
|
||||
)()
|
||||
except:
|
||||
message = "Unable to use `window` or `document` -> https://docs.pyscript.net/latest/faq/#sharedarraybuffer"
|
||||
globalThis.console.warn(message)
|
||||
window = NotSupported("pyscript.window", message)
|
||||
document = NotSupported("pyscript.document", message)
|
||||
js_import = None
|
||||
|
||||
sync = polyscript.xworker.sync
|
||||
|
||||
# in workers the display does not have a default ID
|
||||
# but there is a sync utility from xworker
|
||||
def current_target():
|
||||
return polyscript.target
|
||||
|
||||
else:
|
||||
import _pyscript
|
||||
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
|
||||
sync = NotSupported(
|
||||
"pyscript.sync", "pyscript.sync works only when running in a worker"
|
||||
)
|
||||
|
||||
# in MAIN the current element target exist, just use it
|
||||
def current_target():
|
||||
return _pyscript.target
|
||||
@@ -1,87 +1,247 @@
|
||||
"""
|
||||
This module provides classes and functions for interacting with
|
||||
[media devices and streams](https://developer.mozilla.org/en-US/docs/Web/API/Media_Capture_and_Streams_API)
|
||||
in the browser, enabling you to work with cameras, microphones,
|
||||
and other media input/output devices directly from Python.
|
||||
|
||||
Use this module for:
|
||||
|
||||
- Accessing webcams for video capture.
|
||||
- Recording audio from microphones.
|
||||
- Enumerating available media devices.
|
||||
- Applying constraints to media streams (resolution, frame rate, etc.).
|
||||
|
||||
```python
|
||||
from pyscript import document
|
||||
from pyscript.media import Device, list_devices
|
||||
|
||||
|
||||
# Get a video stream from the default camera.
|
||||
stream = await Device.request_stream(video=True)
|
||||
|
||||
# Display in a video element.
|
||||
video = document.getElementById("my-video")
|
||||
video.srcObject = stream
|
||||
|
||||
# Or list all available devices.
|
||||
devices = await list_devices()
|
||||
for device in devices:
|
||||
print(f"{device.kind}: {device.label}")
|
||||
```
|
||||
|
||||
Using media devices requires user permission. Browsers will show a
|
||||
permission dialog when accessing devices for the first time.
|
||||
"""
|
||||
|
||||
from pyscript import window
|
||||
from pyscript.ffi import to_js
|
||||
|
||||
|
||||
class Device:
|
||||
"""Device represents a media input or output device, such as a microphone,
|
||||
camera, or headset.
|
||||
"""
|
||||
Represents a media input or output device.
|
||||
|
||||
This class wraps a browser
|
||||
[MediaDeviceInfo object](https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo),
|
||||
providing Pythonic access to device properties like `ID`, `label`, and
|
||||
`kind` (audio/video, input/output).
|
||||
|
||||
Devices are typically obtained via the `list_devices()` function in this
|
||||
module, rather than constructed directly.
|
||||
|
||||
```python
|
||||
from pyscript.media import list_devices
|
||||
|
||||
|
||||
# Get all available devices.
|
||||
devices = await list_devices()
|
||||
|
||||
# Find video input devices (cameras).
|
||||
cameras = [d for d in devices if d.kind == "videoinput"]
|
||||
|
||||
# Get a stream from a specific camera.
|
||||
if cameras:
|
||||
stream = await cameras[0].get_stream()
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, device):
|
||||
self._dom_element = device
|
||||
"""
|
||||
Create a Device wrapper around a MediaDeviceInfo `device`.
|
||||
"""
|
||||
self._device_info = device
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._dom_element.deviceId
|
||||
"""
|
||||
Unique identifier for this device.
|
||||
|
||||
This `ID` persists across sessions but is reset when the user clears
|
||||
cookies. It's unique to the origin of the calling application.
|
||||
"""
|
||||
return self._device_info.deviceId
|
||||
|
||||
@property
|
||||
def group(self):
|
||||
return self._dom_element.groupId
|
||||
"""
|
||||
Group identifier for related devices.
|
||||
|
||||
Devices belonging to the same physical device (e.g., a monitor with
|
||||
both a camera and microphone) share the same `group ID`.
|
||||
"""
|
||||
return self._device_info.groupId
|
||||
|
||||
@property
|
||||
def kind(self):
|
||||
return self._dom_element.kind
|
||||
"""
|
||||
Device type: `"videoinput"`, `"audioinput"`, or `"audiooutput"`.
|
||||
"""
|
||||
return self._device_info.kind
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
return self._dom_element.label
|
||||
"""
|
||||
Human-readable description of the device.
|
||||
|
||||
Example: `"External USB Webcam"` or `"Built-in Microphone"`.
|
||||
"""
|
||||
return self._device_info.label
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""
|
||||
Support bracket notation for JavaScript interop.
|
||||
|
||||
Allows accessing properties via `device["id"]` syntax. Necessary
|
||||
when Device instances are proxied to JavaScript.
|
||||
"""
|
||||
return getattr(self, key)
|
||||
|
||||
@classmethod
|
||||
async def request_stream(cls, audio=False, video=True):
|
||||
"""
|
||||
Request a media stream with the specified constraints.
|
||||
|
||||
This is a class method that requests access to media devices matching
|
||||
the given `audio` and `video` constraints. The browser will prompt the
|
||||
user for permission if needed and return a `MediaStream` object that
|
||||
can be assigned to video/audio elements.
|
||||
|
||||
Simple boolean constraints for `audio` and `video` can be used to
|
||||
request default devices. More complex constraints can be specified as
|
||||
dictionaries conforming to
|
||||
[the MediaTrackConstraints interface](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints).
|
||||
|
||||
```python
|
||||
from pyscript import document
|
||||
from pyscript.media import Device
|
||||
|
||||
|
||||
# Get default video stream.
|
||||
stream = await Device.request_stream()
|
||||
|
||||
# Get stream with specific constraints.
|
||||
stream = await Device.request_stream(
|
||||
video={"width": 1920, "height": 1080}
|
||||
)
|
||||
|
||||
# Get audio and video.
|
||||
stream = await Device.request_stream(audio=True, video=True)
|
||||
|
||||
# Use the stream.
|
||||
video_el = document.getElementById("camera")
|
||||
video_el.srcObject = stream
|
||||
```
|
||||
|
||||
This method will trigger a browser permission dialog on first use.
|
||||
"""
|
||||
options = {}
|
||||
if isinstance(audio, bool):
|
||||
options["audio"] = audio
|
||||
elif isinstance(audio, dict):
|
||||
# audio is a dict of constraints (sampleRate, echoCancellation etc...).
|
||||
options["audio"] = audio
|
||||
if isinstance(video, bool):
|
||||
options["video"] = video
|
||||
elif isinstance(video, dict):
|
||||
# video is a dict of constraints (width, height etc...).
|
||||
options["video"] = video
|
||||
return await window.navigator.mediaDevices.getUserMedia(to_js(options))
|
||||
|
||||
@classmethod
|
||||
async def load(cls, audio=False, video=True):
|
||||
"""
|
||||
Load the device stream.
|
||||
!!! warning
|
||||
**Deprecated: Use `request_stream()` instead.**
|
||||
|
||||
This method is retained for backwards compatibility but will be
|
||||
removed in a future release. Please use `request_stream()` instead.
|
||||
"""
|
||||
options = {}
|
||||
options["audio"] = audio
|
||||
if isinstance(video, bool):
|
||||
options["video"] = video
|
||||
else:
|
||||
options["video"] = {}
|
||||
for k in video:
|
||||
options["video"][k] = video[k]
|
||||
return await window.navigator.mediaDevices.getUserMedia(to_js(options))
|
||||
return await cls.request_stream(audio=audio, video=video)
|
||||
|
||||
async def get_stream(self):
|
||||
key = self.kind.replace("input", "").replace("output", "")
|
||||
options = {key: {"deviceId": {"exact": self.id}}}
|
||||
return await self.load(**options)
|
||||
"""
|
||||
Get a media stream from this specific device.
|
||||
|
||||
```python
|
||||
from pyscript.media import list_devices
|
||||
|
||||
|
||||
async def list_devices() -> list[dict]:
|
||||
# List all devices.
|
||||
devices = await list_devices()
|
||||
|
||||
# Find a specific camera.
|
||||
my_camera = None
|
||||
for device in devices:
|
||||
if device.kind == "videoinput" and "USB" in device.label:
|
||||
my_camera = device
|
||||
break
|
||||
|
||||
# Get a stream from that specific camera.
|
||||
if my_camera:
|
||||
stream = await my_camera.get_stream()
|
||||
```
|
||||
|
||||
This will trigger a permission dialog if the user hasn't already
|
||||
granted permission for this device type.
|
||||
"""
|
||||
# Extract media type from device kind (e.g., "videoinput" -> "video").
|
||||
media_type = self.kind.replace("input", "").replace("output", "")
|
||||
# Request stream with exact device ID constraint.
|
||||
options = {media_type: {"deviceId": {"exact": self.id}}}
|
||||
return await self.request_stream(**options)
|
||||
|
||||
|
||||
async def list_devices():
|
||||
"""
|
||||
Return the list of the currently available media input and output devices,
|
||||
such as microphones, cameras, headsets, and so forth.
|
||||
Returns a list of all media devices currently available to the browser,
|
||||
such as microphones, cameras, and speakers.
|
||||
|
||||
Output:
|
||||
```python
|
||||
from pyscript.media import list_devices
|
||||
|
||||
list(dict) - list of dictionaries representing the available media devices.
|
||||
Each dictionary has the following keys:
|
||||
* deviceId: a string that is an identifier for the represented device
|
||||
that is persisted across sessions. It is un-guessable by other
|
||||
applications and unique to the origin of the calling application.
|
||||
It is reset when the user clears cookies (for Private Browsing, a
|
||||
different identifier is used that is not persisted across sessions).
|
||||
|
||||
* groupId: a string that is a group identifier. Two devices have the same
|
||||
group identifier if they belong to the same physical device — for
|
||||
example a monitor with both a built-in camera and a microphone.
|
||||
# Get all devices.
|
||||
devices = await list_devices()
|
||||
|
||||
* kind: an enumerated value that is either "videoinput", "audioinput"
|
||||
or "audiooutput".
|
||||
# Print device information.
|
||||
for device in devices:
|
||||
print(f"{device.kind}: {device.label} (ID: {device.id})")
|
||||
|
||||
* label: a string describing this device (for example "External USB
|
||||
Webcam").
|
||||
# Filter for specific device types.
|
||||
cameras = [d for d in devices if d.kind == "videoinput"]
|
||||
microphones = [d for d in devices if d.kind == "audioinput"]
|
||||
speakers = [d for d in devices if d.kind == "audiooutput"]
|
||||
```
|
||||
|
||||
Note: the returned list will omit any devices that are blocked by the document
|
||||
Permission Policy: microphone, camera, speaker-selection (for output devices),
|
||||
and so on. Access to particular non-default devices is also gated by the
|
||||
Permissions API, and the list will omit devices for which the user has not
|
||||
granted explicit permission.
|
||||
The returned list will omit devices that are blocked by the document
|
||||
[Permission Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Permissions_Policy)
|
||||
(microphone, camera, speaker-selection) or for
|
||||
which the user has not granted explicit permission.
|
||||
|
||||
For security and privacy, device labels may be empty strings until
|
||||
permission is granted. See
|
||||
[this document](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices)
|
||||
for more information about this web standard.
|
||||
"""
|
||||
# https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices
|
||||
return [
|
||||
Device(obj) for obj in await window.navigator.mediaDevices.enumerateDevices()
|
||||
]
|
||||
device_infos = await window.navigator.mediaDevices.enumerateDevices()
|
||||
return [Device(device_info) for device_info in device_infos]
|
||||
|
||||
@@ -1,11 +1,69 @@
|
||||
from polyscript import storage as _storage
|
||||
"""
|
||||
This module wraps the browser's
|
||||
[IndexedDB persistent storage](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
|
||||
to provide a familiar Python dictionary API. Data is automatically
|
||||
serialized and persisted, surviving page reloads and browser restarts.
|
||||
|
||||
Storage is persistent per origin (domain), isolated between different sites
|
||||
for security. Browsers typically allow each origin to store up to 10-60% of
|
||||
total disk space, depending on browser and configuration.
|
||||
|
||||
What this module provides:
|
||||
|
||||
- A `dict`-like API (get, set, delete, iterate).
|
||||
- Automatic serialization of common Python types.
|
||||
- Background persistence with optional explicit `sync()`.
|
||||
- Support for custom `Storage` subclasses.
|
||||
|
||||
```python
|
||||
from pyscript import storage
|
||||
|
||||
|
||||
# Create or open a named storage.
|
||||
my_data = await storage("user-preferences")
|
||||
|
||||
# Use like a regular dictionary.
|
||||
my_data["theme"] = "dark"
|
||||
my_data["font_size"] = 14
|
||||
my_data["settings"] = {"notifications": True, "sound": False}
|
||||
|
||||
# Changes are queued automatically.
|
||||
# To ensure immediate write, sync explicitly.
|
||||
await my_data.sync()
|
||||
|
||||
# Read values (survives page reload).
|
||||
theme = my_data.get("theme", "light")
|
||||
```
|
||||
|
||||
Common types are automatically serialized: `bool`, `int`, `float`, `str`, `None`,
|
||||
`list`, `dict`, `tuple`. Binary data (`bytearray`, `memoryview`) can be stored as
|
||||
single values but not nested in structures.
|
||||
|
||||
Tuples are deserialized as lists due to IndexedDB limitations.
|
||||
|
||||
!!! info
|
||||
Browsers typically allow 10-60% of total disk space per origin. Chrome
|
||||
and Edge allow up to 60%, Firefox up to 10 GiB (or 10% of disk, whichever
|
||||
is smaller). Safari varies by app type. These limits are unlikely to be
|
||||
reached in typical usage.
|
||||
"""
|
||||
|
||||
from polyscript import storage as _polyscript_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):
|
||||
def _convert_to_idb(value):
|
||||
"""
|
||||
Convert a Python `value` to an IndexedDB-compatible format.
|
||||
|
||||
Values are serialized using Flatted (for circular reference support)
|
||||
with type information to enable proper deserialization. It returns a
|
||||
JSON string representing the serialized value.
|
||||
|
||||
Will raise a TypeError if the value type is not supported.
|
||||
"""
|
||||
if is_none(value):
|
||||
return _stringify(["null", 0])
|
||||
if isinstance(value, (bool, float, int, str, list, dict, tuple)):
|
||||
@@ -14,50 +72,179 @@ def _to_idb(value):
|
||||
return _stringify(["bytearray", list(value)])
|
||||
if isinstance(value, memoryview):
|
||||
return _stringify(["memoryview", list(value)])
|
||||
msg = f"Unexpected value: {value}"
|
||||
raise TypeError(msg)
|
||||
raise TypeError(f"Cannot serialize type {type(value).__name__} for storage.")
|
||||
|
||||
|
||||
# convert an IndexedDB compatible entry into a Python value
|
||||
def _from_idb(value):
|
||||
(
|
||||
kind,
|
||||
result,
|
||||
) = _parse(value)
|
||||
def _convert_from_idb(value):
|
||||
"""
|
||||
Convert an IndexedDB `value` back to its Python representation.
|
||||
|
||||
Uses type information stored during serialization to reconstruct the
|
||||
original Python type.
|
||||
"""
|
||||
kind, data = _parse(value)
|
||||
|
||||
if kind == "null":
|
||||
return None
|
||||
if kind == "generic":
|
||||
return result
|
||||
return data
|
||||
if kind == "bytearray":
|
||||
return bytearray(result)
|
||||
return bytearray(data)
|
||||
if kind == "memoryview":
|
||||
return memoryview(bytearray(result))
|
||||
return memoryview(bytearray(data))
|
||||
# Fallback for all other types.
|
||||
return value
|
||||
|
||||
|
||||
class Storage(dict):
|
||||
"""
|
||||
A persistent dictionary backed by the browser's IndexedDB.
|
||||
|
||||
This class provides a dict-like interface with automatic persistence.
|
||||
Changes are queued for background writing, with optional explicit
|
||||
synchronization via `sync()`.
|
||||
|
||||
Inherits from `dict`, so all standard dictionary methods work as expected.
|
||||
|
||||
```python
|
||||
from pyscript import storage
|
||||
|
||||
|
||||
# Open a storage.
|
||||
prefs = await storage("preferences")
|
||||
|
||||
# Use as a dictionary.
|
||||
prefs["color"] = "blue"
|
||||
prefs["count"] = 42
|
||||
|
||||
# Iterate like a dict.
|
||||
for key, value in prefs.items():
|
||||
print(f"{key}: {value}")
|
||||
|
||||
# Ensure writes complete immediately.
|
||||
await prefs.sync()
|
||||
```
|
||||
|
||||
Sometimes you may need to subclass `Storage` to add custom behavior:
|
||||
|
||||
```python
|
||||
from pyscript import storage, Storage, window
|
||||
|
||||
|
||||
class LoggingStorage(Storage):
|
||||
def __setitem__(self, key, value):
|
||||
window.console.log(f"Setting {key} = {value}")
|
||||
super().__setitem__(key, value)
|
||||
|
||||
my_store = await storage("app-data", storage_class=LoggingStorage)
|
||||
my_store["test"] = 123 # Logs to console.
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, store):
|
||||
super().__init__({k: _from_idb(v) for k, v in store.entries()})
|
||||
self.__store__ = store
|
||||
"""
|
||||
Create a Storage instance wrapping an IndexedDB `store` (a JS
|
||||
proxy).
|
||||
"""
|
||||
super().__init__(
|
||||
{key: _convert_from_idb(value) for key, value in store.entries()}
|
||||
)
|
||||
self._store = store
|
||||
|
||||
def __delitem__(self, attr):
|
||||
self.__store__.delete(attr)
|
||||
super().__delitem__(attr)
|
||||
def __delitem__(self, key):
|
||||
"""
|
||||
Delete an item from storage via its `key`.
|
||||
|
||||
def __setitem__(self, attr, value):
|
||||
self.__store__.set(attr, _to_idb(value))
|
||||
super().__setitem__(attr, value)
|
||||
The deletion is queued for persistence. Use `sync()` to ensure
|
||||
immediate completion.
|
||||
"""
|
||||
self._store.delete(key)
|
||||
super().__delitem__(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""
|
||||
Set a `key` to a `value` in storage.
|
||||
|
||||
The change is queued for persistence. Use `sync()` to ensure
|
||||
immediate completion. The `value` must be a supported type for
|
||||
serialization.
|
||||
"""
|
||||
self._store.set(key, _convert_to_idb(value))
|
||||
super().__setitem__(key, value)
|
||||
|
||||
def clear(self):
|
||||
self.__store__.clear()
|
||||
"""
|
||||
Remove all items from storage.
|
||||
|
||||
The `clear()` operation is queued for persistence. Use `sync()` to ensure
|
||||
immediate completion.
|
||||
"""
|
||||
self._store.clear()
|
||||
super().clear()
|
||||
|
||||
async def sync(self):
|
||||
await self.__store__.sync()
|
||||
"""
|
||||
Force immediate synchronization to IndexedDB.
|
||||
|
||||
By default, storage operations are queued and written asynchronously.
|
||||
Call `sync()` when you need to guarantee changes are persisted immediately,
|
||||
such as before critical operations or page unload.
|
||||
|
||||
```python
|
||||
store = await storage("important-data")
|
||||
store["critical_value"] = data
|
||||
|
||||
# Ensure it's written before proceeding.
|
||||
await store.sync()
|
||||
```
|
||||
|
||||
This is a blocking operation that waits for IndexedDB to complete
|
||||
the write.
|
||||
"""
|
||||
await self._store.sync()
|
||||
|
||||
|
||||
async def storage(name="", storage_class=Storage):
|
||||
"""
|
||||
Open or create persistent storage with a unique `name` and optional
|
||||
`storage_class` (used to extend the default `Storage` based behavior).
|
||||
|
||||
Each storage is isolated by name within the current origin (domain).
|
||||
If the storage doesn't exist, it will be created. If it does exist,
|
||||
its current contents will be loaded.
|
||||
|
||||
This function returns a `Storage` instance (or custom subclass instance)
|
||||
acting as a persistent dictionary. A `ValueError` is raised if `name` is
|
||||
empty or not provided.
|
||||
|
||||
```python
|
||||
from pyscript import storage
|
||||
|
||||
|
||||
# Basic usage.
|
||||
user_data = await storage("user-profile")
|
||||
user_data["name"] = "Alice"
|
||||
user_data["age"] = 30
|
||||
|
||||
# Multiple independent storages.
|
||||
settings = await storage("app-settings")
|
||||
cache = await storage("api-cache")
|
||||
|
||||
# With custom Storage class.
|
||||
class ValidatingStorage(Storage):
|
||||
def __setitem__(self, key, value):
|
||||
if not isinstance(key, str):
|
||||
raise TypeError("Keys must be strings")
|
||||
super().__setitem__(key, value)
|
||||
|
||||
validated = await storage("validated-data", ValidatingStorage)
|
||||
```
|
||||
|
||||
Storage names are automatically prefixed with `"@pyscript/"` to
|
||||
namespace them within IndexedDB.
|
||||
"""
|
||||
if not name:
|
||||
msg = "The storage name must be defined"
|
||||
raise ValueError(msg)
|
||||
return storage_class(await _storage(f"@pyscript/{name}"))
|
||||
raise ValueError("Storage name must be a non-empty string")
|
||||
|
||||
underlying_store = await _polyscript_storage(f"@pyscript/{name}")
|
||||
return storage_class(underlying_store)
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
"""
|
||||
This module contains general-purpose utility functions that don't fit into
|
||||
more specific modules. These utilities handle cross-platform compatibility
|
||||
between Pyodide and MicroPython, feature detection, and common type
|
||||
conversions:
|
||||
|
||||
- `as_bytearray`: Convert JavaScript `ArrayBuffer` to Python `bytearray`.
|
||||
- `NotSupported`: Placeholder for unavailable features in specific contexts.
|
||||
- `is_awaitable`: Detect `async` functions across Python implementations.
|
||||
|
||||
These utilities are primarily used internally by PyScript but are available
|
||||
for use in application code when needed.
|
||||
"""
|
||||
|
||||
import js
|
||||
import sys
|
||||
import inspect
|
||||
|
||||
|
||||
def as_bytearray(buffer):
|
||||
"""
|
||||
Given a JavaScript ArrayBuffer, convert it to a Python bytearray in a
|
||||
Given a JavaScript `ArrayBuffer`, convert it to a Python `bytearray` in a
|
||||
MicroPython friendly manner.
|
||||
"""
|
||||
ui8a = js.Uint8Array.new(buffer)
|
||||
@@ -42,17 +55,25 @@ class NotSupported:
|
||||
def is_awaitable(obj):
|
||||
"""
|
||||
Returns a boolean indication if the passed in obj is an awaitable
|
||||
function. (MicroPython treats awaitables as generator functions, and if
|
||||
the object is a closure containing an async function we need to work
|
||||
carefully.)
|
||||
function. This is interpreter agnostic.
|
||||
|
||||
!!! info
|
||||
MicroPython treats awaitables as generator functions, and if
|
||||
the object is a closure containing an async function or a bound method
|
||||
we need to work carefully.
|
||||
"""
|
||||
from pyscript import config
|
||||
|
||||
if config["type"] == "mpy": # Is MicroPython?
|
||||
if config["type"] == "mpy":
|
||||
# MicroPython doesn't appear to have a way to determine if a closure is
|
||||
# an async function except via the repr. This is a bit hacky.
|
||||
if "<closure <generator>" in repr(obj):
|
||||
r = repr(obj)
|
||||
if "<closure <generator>" in r:
|
||||
return True
|
||||
# Same applies to bound methods.
|
||||
if "<bound_method" in r and "<generator>" in r:
|
||||
return True
|
||||
# In MicroPython, generator functions are awaitable.
|
||||
return inspect.isgeneratorfunction(obj)
|
||||
|
||||
return inspect.iscoroutinefunction(obj)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,88 +1,295 @@
|
||||
"""
|
||||
This module provides a Pythonic wrapper around the browser's
|
||||
[WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket),
|
||||
enabling two-way communication with WebSocket servers.
|
||||
|
||||
Use this for real-time applications:
|
||||
|
||||
- Pythonic interface to browser WebSockets.
|
||||
- Automatic handling of async event handlers.
|
||||
- Support for receiving text (`str`) and binary (`memoryview`) data.
|
||||
- Support for sending text (`str`) and binary (`bytes` and `bytearray`) data.
|
||||
- Compatible with Pyodide and MicroPython.
|
||||
- Works in webworker contexts.
|
||||
- Naming deliberately follows the JavaScript WebSocket API closely for
|
||||
familiarity.
|
||||
|
||||
See the Python docs for
|
||||
[an explanation of memoryview](https://docs.python.org/3/library/stdtypes.html#memoryview).
|
||||
|
||||
|
||||
```python
|
||||
from pyscript import WebSocket
|
||||
|
||||
|
||||
def on_open(event):
|
||||
print("Connected!")
|
||||
ws.send("Hello server")
|
||||
|
||||
def on_message(event):
|
||||
print(f"Received: {event.data}")
|
||||
|
||||
def on_close(event):
|
||||
print("Connection closed")
|
||||
|
||||
ws = WebSocket(url="ws://localhost:8080/")
|
||||
ws.onopen = on_open
|
||||
ws.onmessage = on_message
|
||||
ws.onclose = on_close
|
||||
```
|
||||
"""
|
||||
|
||||
import js
|
||||
from pyscript.ffi import create_proxy
|
||||
from pyscript.util import as_bytearray, is_awaitable
|
||||
|
||||
code = "code"
|
||||
protocols = "protocols"
|
||||
reason = "reason"
|
||||
methods = ["onclose", "onerror", "onmessage", "onopen"]
|
||||
|
||||
def _attach_event_handler(websocket, handler_name, handler_function):
|
||||
"""
|
||||
Given a `websocket`, and `handler_name`, attach the `handler_function`
|
||||
to the `WebSocket` instance, handling both synchronous and asynchronous
|
||||
handler functions.
|
||||
|
||||
def add_listener(socket, onevent, listener):
|
||||
p = create_proxy(listener)
|
||||
Creates a JavaScript proxy for the handler and wraps async handlers
|
||||
appropriately. Handles the `WebSocketEvent` wrapping for all handlers.
|
||||
"""
|
||||
if is_awaitable(handler_function):
|
||||
|
||||
if is_awaitable(listener):
|
||||
|
||||
async def wrapper(e):
|
||||
await p(EventMessage(e))
|
||||
|
||||
m = wrapper
|
||||
async def async_wrapper(event):
|
||||
await handler_function(WebSocketEvent(event))
|
||||
|
||||
wrapped_handler = create_proxy(async_wrapper)
|
||||
else:
|
||||
m = lambda e: p(EventMessage(e))
|
||||
|
||||
# Pyodide fails at setting socket[onevent] directly
|
||||
setattr(socket, onevent, m)
|
||||
wrapped_handler = create_proxy(
|
||||
lambda event: handler_function(WebSocketEvent(event))
|
||||
)
|
||||
# Note: Direct assignment (websocket[handler_name]) fails in Pyodide.
|
||||
setattr(websocket, handler_name, wrapped_handler)
|
||||
|
||||
|
||||
class EventMessage:
|
||||
class WebSocketEvent:
|
||||
"""
|
||||
A read-only wrapper for
|
||||
[WebSocket event objects](https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent).
|
||||
|
||||
This class wraps browser WebSocket events and provides convenient access
|
||||
to event properties. It handles the conversion of binary data from
|
||||
JavaScript typed arrays to Python bytes-like objects.
|
||||
|
||||
The most commonly used property is `event.data`, which contains the
|
||||
message data for "message" events.
|
||||
|
||||
```python
|
||||
def on_message(event): # The event is a WebSocketEvent instance.
|
||||
# For text messages.
|
||||
if isinstance(event.data, str):
|
||||
print(f"Text: {event.data}")
|
||||
else:
|
||||
# For binary messages.
|
||||
print(f"Binary: {len(event.data)} bytes")
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, event):
|
||||
"""
|
||||
Create a WebSocketEvent wrapper from an underlying JavaScript
|
||||
`event`.
|
||||
"""
|
||||
self._event = event
|
||||
|
||||
def __getattr__(self, attr):
|
||||
value = getattr(self._event, attr)
|
||||
"""
|
||||
Get an attribute `attr` from the underlying event object.
|
||||
|
||||
Handles special conversion of binary data from JavaScript typed
|
||||
arrays to Python `memoryview` objects.
|
||||
"""
|
||||
value = getattr(self._event, attr)
|
||||
if attr == "data" and not isinstance(value, str):
|
||||
if hasattr(value, "to_py"):
|
||||
# Pyodide - convert JavaScript typed array to Python.
|
||||
return value.to_py()
|
||||
# shims in MicroPython
|
||||
return memoryview(as_bytearray(value))
|
||||
|
||||
else:
|
||||
# MicroPython - manually convert JS ArrayBuffer.
|
||||
return memoryview(as_bytearray(value))
|
||||
return value
|
||||
|
||||
|
||||
class WebSocket:
|
||||
"""
|
||||
This class provides a Python-friendly interface to WebSocket connections,
|
||||
handling communication with WebSocket servers. It supports both text and
|
||||
binary data transmission.
|
||||
|
||||
Access the underlying WebSocket methods and properties directly if needed.
|
||||
However, the wrapper provides a more Pythonic API. If you need to work
|
||||
with the raw JavaScript WebSocket instance, you can access it via the
|
||||
`_js_websocket` attribute.
|
||||
|
||||
Using textual (`str`) data:
|
||||
|
||||
```python
|
||||
from pyscript import WebSocket
|
||||
|
||||
|
||||
# Create WebSocket with handlers as arguments.
|
||||
def handle_message(event):
|
||||
print(f"Got: {event.data}")
|
||||
|
||||
ws = WebSocket(
|
||||
url="ws://echo.websocket.org/",
|
||||
onmessage=handle_message
|
||||
)
|
||||
|
||||
# Or assign handlers after creation.
|
||||
def handle_open(event):
|
||||
ws.send("Hello!")
|
||||
|
||||
ws.onopen = handle_open
|
||||
```
|
||||
|
||||
Using binary (`memoryview`) data:
|
||||
|
||||
```python
|
||||
def handle_message(event):
|
||||
if isinstance(event.data, str):
|
||||
print(f"Text: {event.data}")
|
||||
else:
|
||||
# Binary data as memoryview.
|
||||
print(f"Binary: {len(event.data)} bytes")
|
||||
|
||||
ws = WebSocket(url="ws://example.com/", onmessage=handle_message)
|
||||
|
||||
# Send binary data.
|
||||
data = bytearray([0x01, 0x02, 0x03])
|
||||
ws.send(data)
|
||||
```
|
||||
|
||||
Read more about Python's
|
||||
[`memoryview` here](https://docs.python.org/3/library/stdtypes.html#memoryview).
|
||||
"""
|
||||
|
||||
# WebSocket ready state constants.
|
||||
CONNECTING = 0
|
||||
OPEN = 1
|
||||
CLOSING = 2
|
||||
CLOSED = 3
|
||||
|
||||
def __init__(self, **kw):
|
||||
url = kw["url"]
|
||||
if protocols in kw:
|
||||
socket = js.WebSocket.new(url, kw[protocols])
|
||||
def __init__(self, url, protocols=None, **handlers):
|
||||
"""
|
||||
Create a new WebSocket connection from the given `url` (`ws://` or
|
||||
`wss://`). Optionally specify `protocols` (a string or a list of
|
||||
protocol strings) and event handlers (`onopen`, `onmessage`, etc.) as
|
||||
keyword arguments.
|
||||
|
||||
These arguments and naming conventions mirror those of the
|
||||
[underlying JavaScript WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
|
||||
for familiarity.
|
||||
|
||||
If you need access to the underlying JavaScript WebSocket instance,
|
||||
you can get it via the `_js_websocket` attribute.
|
||||
|
||||
```python
|
||||
# Basic connection.
|
||||
ws = WebSocket(url="ws://localhost:8080/")
|
||||
|
||||
# With protocol.
|
||||
ws = WebSocket(
|
||||
url="wss://example.com/socket",
|
||||
protocols="chat"
|
||||
)
|
||||
|
||||
# With handlers.
|
||||
ws = WebSocket(
|
||||
url="ws://localhost:8080/",
|
||||
onopen=lambda e: print("Connected"),
|
||||
onmessage=lambda e: print(e.data)
|
||||
)
|
||||
```
|
||||
"""
|
||||
# Create underlying JavaScript WebSocket.
|
||||
if protocols:
|
||||
js_websocket = js.WebSocket.new(url, protocols)
|
||||
else:
|
||||
socket = js.WebSocket.new(url)
|
||||
|
||||
socket.binaryType = "arraybuffer"
|
||||
object.__setattr__(self, "_ws", socket)
|
||||
|
||||
for t in methods:
|
||||
if t in kw:
|
||||
add_listener(socket, t, kw[t])
|
||||
js_websocket = js.WebSocket.new(url)
|
||||
# Set binary type to arraybuffer for easier Python handling.
|
||||
js_websocket.binaryType = "arraybuffer"
|
||||
# Store the underlying WebSocket.
|
||||
# Use object.__setattr__ to bypass our custom __setattr__.
|
||||
object.__setattr__(self, "_js_websocket", js_websocket)
|
||||
# Attach any event handlers passed as keyword arguments.
|
||||
for handler_name, handler in handlers.items():
|
||||
setattr(self, handler_name, handler)
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self._ws, attr)
|
||||
"""
|
||||
Get an attribute `attr` from the underlying WebSocket.
|
||||
|
||||
This allows transparent access to WebSocket properties like
|
||||
`readyState`, `url`, `bufferedAmount`, etc.
|
||||
"""
|
||||
return getattr(self._js_websocket, attr)
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
if attr in methods:
|
||||
add_listener(self._ws, attr, value)
|
||||
else:
|
||||
setattr(self._ws, attr, value)
|
||||
"""
|
||||
Set an attribute `attr` on the WebSocket to the given `value`.
|
||||
|
||||
def close(self, **kw):
|
||||
if code in kw and reason in kw:
|
||||
self._ws.close(kw[code], kw[reason])
|
||||
elif code in kw:
|
||||
self._ws.close(kw[code])
|
||||
Event handler attributes (`onopen`, `onmessage`, etc.) are specially
|
||||
handled to create proper proxies. Other attributes are set on the
|
||||
underlying WebSocket directly.
|
||||
"""
|
||||
if attr in ["onclose", "onerror", "onmessage", "onopen"]:
|
||||
_attach_event_handler(self._js_websocket, attr, value)
|
||||
else:
|
||||
self._ws.close()
|
||||
setattr(self._js_websocket, attr, value)
|
||||
|
||||
def send(self, data):
|
||||
"""
|
||||
Send `data` through the WebSocket.
|
||||
|
||||
Accepts both text (`str`) and binary data (`bytes`, `bytearray`, etc.).
|
||||
Binary data is automatically converted to a JavaScript `Uint8Array`.
|
||||
|
||||
```python
|
||||
# Send text.
|
||||
ws.send("Hello server!")
|
||||
|
||||
# Send binary.
|
||||
ws.send(bytes([1, 2, 3, 4]))
|
||||
ws.send(bytearray([5, 6, 7, 8]))
|
||||
```
|
||||
|
||||
!!! warning
|
||||
|
||||
The WebSocket **must be in the OPEN state to send data**.
|
||||
"""
|
||||
if isinstance(data, str):
|
||||
self._ws.send(data)
|
||||
self._js_websocket.send(data)
|
||||
else:
|
||||
buffer = js.Uint8Array.new(len(data))
|
||||
for pos, b in enumerate(data):
|
||||
buffer[pos] = b
|
||||
self._ws.send(buffer)
|
||||
for index, byte_value in enumerate(data):
|
||||
buffer[index] = byte_value
|
||||
self._js_websocket.send(buffer)
|
||||
|
||||
def close(self, code=None, reason=None):
|
||||
"""
|
||||
Close the WebSocket connection. Optionally specify a `code` (`int`)
|
||||
and a `reason` (`str`) for closing the connection.
|
||||
|
||||
```python
|
||||
# Normal close.
|
||||
ws.close()
|
||||
|
||||
# Close with code and reason.
|
||||
ws.close(code=1000, reason="Task completed")
|
||||
```
|
||||
|
||||
Usage and values for `code` and `reasons`
|
||||
[are explained here](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close).
|
||||
"""
|
||||
if code and reason:
|
||||
self._js_websocket.close(code, reason)
|
||||
elif code:
|
||||
self._js_websocket.close(code)
|
||||
else:
|
||||
self._js_websocket.close()
|
||||
|
||||
@@ -1,45 +1,194 @@
|
||||
import js as _js
|
||||
from polyscript import workers as _workers
|
||||
"""
|
||||
This module provides access to named
|
||||
[web workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API)
|
||||
defined in `<script>` tags, and utilities for dynamically creating workers
|
||||
from Python code.
|
||||
|
||||
_get = _js.Reflect.get
|
||||
Named workers are Python web workers defined in HTML with a `name` attribute
|
||||
that can be referenced from the main thread or other workers. This module
|
||||
provides the `workers` object for accessing named workers and the
|
||||
`create_named_worker()` function for dynamically creating them.
|
||||
|
||||
Accessing named workers:
|
||||
|
||||
```html
|
||||
<!-- Define a named worker -->
|
||||
<script type="py" worker name="calculator">
|
||||
def add(a, b):
|
||||
return a + b
|
||||
|
||||
__export__ = ["add"]
|
||||
</script>
|
||||
|
||||
<!-- Access from main thread -->
|
||||
<script type="mpy">
|
||||
from pyscript import workers
|
||||
|
||||
|
||||
def _set(script, name, value=""):
|
||||
script.setAttribute(name, value)
|
||||
calc = await workers["calculator"]
|
||||
result = await calc.add(5, 3)
|
||||
print(result) # 8
|
||||
</script>
|
||||
```
|
||||
|
||||
Dynamically creating named workers:
|
||||
|
||||
```python
|
||||
from pyscript import create_named_worker
|
||||
|
||||
|
||||
# this solves an inconsistency between Pyodide and MicroPython
|
||||
# @see https://github.com/pyscript/pyscript/issues/2106
|
||||
class _ReadOnlyProxy:
|
||||
# Create a worker from a Python file.
|
||||
worker = await create_named_worker(
|
||||
src="./background_tasks.py",
|
||||
name="task-processor"
|
||||
)
|
||||
|
||||
# Use the worker's exported functions.
|
||||
result = await worker.process_data([1, 2, 3, 4, 5])
|
||||
print(result)
|
||||
```
|
||||
|
||||
Key features:
|
||||
- Access (`await`) named workers via dictionary-like syntax.
|
||||
- Dynamically create workers from Python.
|
||||
- Cross-interpreter support (Pyodide and MicroPython).
|
||||
|
||||
Worker access is asynchronous - you must `await workers[name]` to get
|
||||
a reference to the worker. This is because workers may not be ready
|
||||
immediately at startup.
|
||||
"""
|
||||
|
||||
import js
|
||||
import json
|
||||
from polyscript import workers as _polyscript_workers
|
||||
|
||||
|
||||
class _ReadOnlyWorkersProxy:
|
||||
"""
|
||||
A read-only proxy for accessing named web workers. Use
|
||||
`create_named_worker()` to create new workers found in this proxy.
|
||||
|
||||
This provides dictionary-like access to named workers defined in
|
||||
the page. It handles differences between Pyodide and MicroPython
|
||||
implementations transparently.
|
||||
|
||||
(See: https://github.com/pyscript/pyscript/issues/2106 for context.)
|
||||
|
||||
The proxy is read-only to prevent accidental modification of the
|
||||
underlying workers registry. Both item access and attribute access are
|
||||
supported for convenience (especially since HTML attribute names may
|
||||
not be valid Python identifiers).
|
||||
|
||||
```python
|
||||
from pyscript import workers
|
||||
|
||||
# Access a named worker.
|
||||
my_worker = await workers["worker-name"]
|
||||
result = await my_worker.some_function()
|
||||
|
||||
# Alternatively, if the name works, access via attribute notation.
|
||||
my_worker = await workers.worker_name
|
||||
result = await my_worker.some_function()
|
||||
```
|
||||
|
||||
**This is a proxy object, not a dict**. You cannot iterate over it or
|
||||
get a list of worker names. This is intentional because worker
|
||||
startup timing is non-deterministic.
|
||||
"""
|
||||
|
||||
def __getitem__(self, name):
|
||||
return _get(_workers, name)
|
||||
"""
|
||||
Get a named worker by `name`. It returns a promise that resolves to
|
||||
the worker reference when ready.
|
||||
|
||||
This is useful if the underlying worker name is not a valid Python
|
||||
identifier.
|
||||
|
||||
```python
|
||||
worker = await workers["my-worker"]
|
||||
```
|
||||
"""
|
||||
return js.Reflect.get(_polyscript_workers, name)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return _get(_workers, name)
|
||||
"""
|
||||
Get a named worker as an attribute. It returns a promise that resolves
|
||||
to the worker reference when ready.
|
||||
|
||||
This allows accessing workers via dot notation as an alternative
|
||||
to bracket notation.
|
||||
|
||||
```python
|
||||
worker = await workers.my_worker
|
||||
```
|
||||
"""
|
||||
return js.Reflect.get(_polyscript_workers, name)
|
||||
|
||||
|
||||
workers = _ReadOnlyProxy()
|
||||
# Global workers proxy for accessing named workers.
|
||||
workers = _ReadOnlyWorkersProxy()
|
||||
"""Global proxy for accessing named web workers."""
|
||||
|
||||
|
||||
async def create_named_worker(src="", name="", config=None, type="py"):
|
||||
from json import dumps
|
||||
async def create_named_worker(src, name, config=None, type="py"):
|
||||
"""
|
||||
Dynamically create a web worker with a `src` Python file, a unique
|
||||
`name` and optional `config` (dict or JSON string) and `type` (`py`
|
||||
for Pyodide or `mpy` for MicroPython, the default is `py`).
|
||||
|
||||
if not src:
|
||||
msg = "Named workers require src"
|
||||
raise ValueError(msg)
|
||||
This function creates a new web worker by injecting a `<script>` tag into
|
||||
the document. The worker will be accessible via the `workers` proxy once
|
||||
it's ready.
|
||||
|
||||
if not name:
|
||||
msg = "Named workers require a name"
|
||||
raise ValueError(msg)
|
||||
It returns a promise that resolves to the worker reference when ready.
|
||||
|
||||
s = _js.document.createElement("script")
|
||||
s.type = type
|
||||
s.src = src
|
||||
_set(s, "worker")
|
||||
_set(s, "name", name)
|
||||
```python
|
||||
from pyscript import create_named_worker
|
||||
|
||||
|
||||
# Create a Pyodide worker.
|
||||
worker = await create_named_worker(
|
||||
src="./my_worker.py",
|
||||
name="background-worker"
|
||||
)
|
||||
|
||||
# Use the worker.
|
||||
result = await worker.process_data()
|
||||
|
||||
# Create with standard PyScript configuration.
|
||||
worker = await create_named_worker(
|
||||
src="./processor.py",
|
||||
name="data-processor",
|
||||
config={"packages": ["numpy", "pandas"]}
|
||||
)
|
||||
|
||||
# Use MicroPython instead.
|
||||
worker = await create_named_worker(
|
||||
src="./lightweight_worker.py",
|
||||
name="micro-worker",
|
||||
type="mpy"
|
||||
)
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
**The worker script should define** `__export__` to specify which
|
||||
functions or objects are accessible from the main thread.
|
||||
"""
|
||||
# Create script element for the worker.
|
||||
script = js.document.createElement("script")
|
||||
script.type = type
|
||||
script.src = src
|
||||
# Mark as a worker with a name.
|
||||
script.setAttribute("worker", "")
|
||||
script.setAttribute("name", name)
|
||||
# Add configuration if provided.
|
||||
if config:
|
||||
_set(s, "config", (isinstance(config, str) and config) or dumps(config))
|
||||
|
||||
_js.document.body.append(s)
|
||||
if isinstance(config, str):
|
||||
config_str = config
|
||||
else:
|
||||
config_str = json.dumps(config)
|
||||
script.setAttribute("config", config_str)
|
||||
# Inject the script into the document and await the result.
|
||||
js.document.body.append(script)
|
||||
return await workers[name]
|
||||
|
||||
Reference in New Issue
Block a user