diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 70fe6fb5..43d9ea56 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -11,7 +11,6 @@ repos:
hooks:
- id: check-builtin-literals
- id: check-case-conflict
- - id: check-docstring-first
- id: check-executables-have-shebangs
- id: check-json
exclude: tsconfig\.json
diff --git a/core/src/stdlib/pyscript.js b/core/src/stdlib/pyscript.js
index fcac6257..840212eb 100644
--- a/core/src/stdlib/pyscript.js
+++ b/core/src/stdlib/pyscript.js
@@ -1,19 +1,19 @@
// ⚠️ This file is an artifact: DO NOT MODIFY
export default {
"pyscript": {
- "__init__.py": "from polyscript import lazy_py_modules as py_import\nfrom pyscript.magic_js import RUNNING_IN_WORKER,PyWorker,config,current_target,document,js_import,js_modules,sync,window\nfrom pyscript.display import HTML,display\nfrom pyscript.fetch import fetch\nfrom pyscript.storage import Storage,storage\nfrom pyscript.websocket import WebSocket\nfrom pyscript.events import when,Event\nif not RUNNING_IN_WORKER:from pyscript.workers import create_named_worker,workers",
- "display.py": "_K='_repr_mimebundle_'\n_J='image/svg+xml'\n_I='application/json'\n_H='__repr__'\n_G='savefig'\n_F='text/html'\n_E='image/jpeg'\n_D='application/javascript'\n_C='utf-8'\n_B='text/plain'\n_A='image/png'\nimport base64,html,io,re\nfrom pyscript.magic_js import current_target,document,window\nfrom pyscript.ffi import is_none\n_MIME_METHODS={_G:_A,'_repr_javascript_':_D,'_repr_json_':_I,'_repr_latex':'text/latex','_repr_png_':_A,'_repr_jpeg_':_E,'_repr_pdf_':'application/pdf','_repr_svg_':_J,'_repr_markdown_':'text/markdown','_repr_html_':_F,_H:_B}\ndef _render_image(mime,value,meta):\n\tA=value\n\tif isinstance(A,bytes):A=base64.b64encode(A).decode(_C)\n\tB=re.compile('^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$')\n\tif len(A)>0 and not B.match(A):A=base64.b64encode(A.encode(_C)).decode(_C)\n\tC=f\"data:{mime};charset=utf-8;base64,{A}\";D=' '.join(['{k}=\"{v}\"'for(A,B)in meta.items()]);return f''\ndef _identity(value,meta):return value\n_MIME_RENDERERS={_B:html.escape,_F:_identity,_A:lambda value,meta:_render_image(_A,value,meta),_E:lambda value,meta:_render_image(_E,value,meta),_J:_identity,_I:_identity,_D:lambda value,meta:f\"
+
+
+
+```
+
+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 `
+
Test Read and Write
Content test_rr_div
@@ -63,6 +65,7 @@
+
diff --git a/core/tests/python/settings_mpy.json b/core/tests/python/settings_mpy.json
index fb46cf3b..255f6f9c 100644
--- a/core/tests/python/settings_mpy.json
+++ b/core/tests/python/settings_mpy.json
@@ -1,27 +1,26 @@
{
"files": {
- "https://raw.githubusercontent.com/ntoll/upytest/1.0.9/upytest.py": "",
+ "https://raw.githubusercontent.com/ntoll/upytest/1.0.11/upytest.py": "",
"./tests/test_config.py": "tests/test_config.py",
+ "./tests/test_context.py": "tests/test_context.py",
"./tests/test_current_target.py": "tests/test_current_target.py",
"./tests/test_display.py": "tests/test_display.py",
"./tests/test_document.py": "tests/test_document.py",
+ "./tests/test_events.py": "tests/test_events.py",
"./tests/test_fetch.py": "tests/test_fetch.py",
"./tests/test_ffi.py": "tests/test_ffi.py",
- "./tests/test_js_modules.py": "tests/test_js_modules.py",
+ "./tests/test_fs.py": "tests/test_fs.py",
"./tests/test_media.py": "tests/test_media.py",
"./tests/test_storage.py": "tests/test_storage.py",
+ "./tests/test_util.py": "tests/test_util.py",
"./tests/test_running_in_worker.py": "tests/test_running_in_worker.py",
"./tests/test_web.py": "tests/test_web.py",
"./tests/test_websocket.py": "tests/test_websocket.py",
- "./tests/test_events.py": "tests/test_events.py",
- "./tests/test_window.py": "tests/test_window.py"
+ "./tests/test_window.py": "tests/test_window.py",
+ "./tests/test_workers.py": "tests/test_workers.py"
},
"js_modules": {
- "main": {
- "./example_js_module.js": "greeting"
- },
- "worker": {
- "./example_js_worker_module.js": "greeting_worker"
- }
+ "main": {"./example_js_module.js": "greeting"},
+ "worker": {"./example_js_worker_module.js": "greeting_worker"}
}
}
diff --git a/core/tests/python/settings_py.json b/core/tests/python/settings_py.json
index 50086a8c..af3e03c6 100644
--- a/core/tests/python/settings_py.json
+++ b/core/tests/python/settings_py.json
@@ -1,29 +1,28 @@
{
"files": {
- "https://raw.githubusercontent.com/ntoll/upytest/1.0.9/upytest.py": "",
+ "https://raw.githubusercontent.com/ntoll/upytest/1.0.11/upytest.py": "",
"./tests/test_config.py": "tests/test_config.py",
+ "./tests/test_context.py": "tests/test_context.py",
"./tests/test_current_target.py": "tests/test_current_target.py",
"./tests/test_display.py": "tests/test_display.py",
"./tests/test_document.py": "tests/test_document.py",
+ "./tests/test_events.py": "tests/test_events.py",
"./tests/test_fetch.py": "tests/test_fetch.py",
"./tests/test_ffi.py": "tests/test_ffi.py",
+ "./tests/test_fs.py": "tests/test_fs.py",
"./tests/test_media.py": "tests/test_media.py",
- "./tests/test_js_modules.py": "tests/test_js_modules.py",
"./tests/test_storage.py": "tests/test_storage.py",
+ "./tests/test_util.py": "tests/test_util.py",
"./tests/test_running_in_worker.py": "tests/test_running_in_worker.py",
"./tests/test_web.py": "tests/test_web.py",
"./tests/test_websocket.py": "tests/test_websocket.py",
- "./tests/test_events.py": "tests/test_events.py",
- "./tests/test_window.py": "tests/test_window.py"
+ "./tests/test_window.py": "tests/test_window.py",
+ "./tests/test_workers.py": "tests/test_workers.py"
},
"js_modules": {
- "main": {
- "./example_js_module.js": "greeting"
- },
- "worker": {
- "./example_js_worker_module.js": "greeting_worker"
- }
+ "main": {"./example_js_module.js": "greeting"},
+ "worker": {"./example_js_worker_module.js": "greeting_worker"}
},
- "packages": ["Pillow" ],
+ "packages": ["Pillow"],
"experimental_ffi_timeout": 0
}
diff --git a/core/tests/python/tests/test_js_modules.py b/core/tests/python/tests/test_context.py
similarity index 100%
rename from core/tests/python/tests/test_js_modules.py
rename to core/tests/python/tests/test_context.py
diff --git a/core/tests/python/tests/test_current_target.py b/core/tests/python/tests/test_current_target.py
index cc4e24d2..fa2daf61 100644
--- a/core/tests/python/tests/test_current_target.py
+++ b/core/tests/python/tests/test_current_target.py
@@ -13,7 +13,7 @@ def test_current_target():
"""
expected = "py-0"
if is_micropython:
- expected = "mpy-w0-target" if RUNNING_IN_WORKER else "mpy-0"
+ expected = "mpy-w1-target" if RUNNING_IN_WORKER else "mpy-0"
elif RUNNING_IN_WORKER:
- expected = "py-w0-target"
+ expected = "py-w1-target"
assert current_target() == expected, f"Expected {expected} got {current_target()}"
diff --git a/core/tests/python/tests/test_display.py b/core/tests/python/tests/test_display.py
index d891bf7c..ada84582 100644
--- a/core/tests/python/tests/test_display.py
+++ b/core/tests/python/tests/test_display.py
@@ -3,6 +3,7 @@ Tests for the display function in PyScript.
"""
import asyncio
+import json
import upytest
from pyscript import HTML, RUNNING_IN_WORKER, display, py_import, web
@@ -107,20 +108,7 @@ def test_empty_string_target_raises_value_error():
"""
with upytest.raises(ValueError) as exc:
display("hello world", target="")
- assert str(exc.exception) == "Cannot have an empty target"
-
-
-def test_non_string_target_values_raise_typerror():
- """
- The target parameter must be a string.
- """
- with upytest.raises(TypeError) as exc:
- display("hello world", target=True)
- assert str(exc.exception) == "target must be str or None, not bool"
-
- with upytest.raises(TypeError) as exc:
- display("hello world", target=123)
- assert str(exc.exception) == "target must be str or None, not int"
+ assert str(exc.exception) == "Cannot find element with id='' in the page."
async def test_tag_target_attribute():
@@ -286,4 +274,406 @@ async def test_image_renders_correctly():
display(img, target="test-element-container", append=False)
target = web.page.find("#test-element-container")[0]
img = target.find("img")[0]
- assert img.src.startswith("data:image/png;charset=utf-8;base64")
+ assert img.src.startswith("data:image/png;base64"), img.src
+
+
+async def test_mimebundle_simple():
+ """
+ An object with _repr_mimebundle_ should use the mimebundle formats.
+ """
+
+ class MimebundleObj:
+ def _repr_mimebundle_(self):
+ return {
+ "text/html": "Bold HTML",
+ "text/plain": "Plain text fallback",
+ }
+
+ display(MimebundleObj())
+ container = await get_display_container()
+ # Should prefer HTML from mimebundle.
+ assert container[0].innerHTML == "Bold HTML"
+
+
+async def test_mimebundle_with_metadata():
+ """
+ Mimebundle can include metadata for specific MIME types.
+ """
+
+ class ImageWithMeta:
+ def _repr_mimebundle_(self):
+ return (
+ {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
+ },
+ {"image/png": {"width": "100", "height": "50"}},
+ )
+
+ display(ImageWithMeta(), target="test-element-container", append=False)
+ target = web.page.find("#test-element-container")[0]
+ img = target.find("img")[0]
+ assert img.getAttribute("width") == "100"
+ assert img.getAttribute("height") == "50"
+
+
+async def test_mimebundle_with_tuple_output():
+ """
+ Mimebundle format values can be tuples with (data, metadata).
+ """
+
+ class TupleOutput:
+ def _repr_mimebundle_(self):
+ return {"text/html": ("Italic", {"custom": "meta"})}
+
+ display(TupleOutput())
+ container = await get_display_container()
+ assert container[0].innerHTML == "Italic"
+
+
+async def test_mimebundle_metadata_merge():
+ """
+ Format-specific metadata should merge with global metadata.
+ """
+
+ class MetaMerge:
+ def _repr_mimebundle_(self):
+ return (
+ {
+ "image/png": (
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
+ {"height": "75"},
+ )
+ },
+ {"image/png": {"width": "100"}},
+ )
+
+ display(MetaMerge(), target="test-element-container", append=False)
+ target = web.page.find("#test-element-container")[0]
+ img = target.find("img")[0]
+ # Both global and format-specific metadata should be present.
+ assert img.getAttribute("width") == "100"
+ assert img.getAttribute("height") == "75"
+
+
+async def test_mimebundle_unsupported_mime():
+ """
+ If mimebundle contains only unsupported MIME types, fall back to regular methods.
+ """
+
+ class UnsupportedMime:
+ def _repr_mimebundle_(self):
+ return {"application/pdf": "PDF data", "text/latex": "LaTeX data"}
+
+ def _repr_html_(self):
+ return "
HTML fallback
"
+
+ display(UnsupportedMime())
+ container = await get_display_container()
+ # Should fall back to _repr_html_.
+ assert container[0].innerHTML == "
HTML fallback
"
+
+
+async def test_mimebundle_no_dict():
+ """
+ Mimebundle that returns just a dict (no tuple) should work.
+ """
+
+ class SimpleMimebundle:
+ def _repr_mimebundle_(self):
+ return {"text/html": "Code"}
+
+ display(SimpleMimebundle())
+ container = await get_display_container()
+ assert container[0].innerHTML == "Code"
+
+
+async def test_repr_html():
+ """
+ Objects with _repr_html_ should render as HTML.
+ """
+
+ class HTMLRepr:
+ def _repr_html_(self):
+ return "
", {"data-custom": "value"})
+
+ display(HTMLWithMeta())
+ container = await get_display_container()
+ # Metadata is not used in _repr_html_ rendering, but ensure HTML is
+ # correct.
+ assert container[0].innerHTML == "
Paragraph
"
+
+
+async def test_repr_svg():
+ """
+ Objects with _repr_svg_ should render as SVG.
+ """
+
+ class SVGRepr:
+ def _repr_svg_(self):
+ return ''
+
+ display(SVGRepr(), target="test-element-container", append=False)
+ target = web.page.find("#test-element-container")[0]
+ assert "svg" in target.innerHTML.lower()
+ assert "circle" in target.innerHTML.lower()
+
+
+async def test_repr_json():
+ """
+ Objects with _repr_json_ should render as JSON.
+ """
+
+ class JSONRepr:
+ def _repr_json_(self):
+ return '{"key": "value", "number": 42}'
+
+ display(JSONRepr(), target="test-element-container", append=False)
+ target = web.page.find("#test-element-container")[0]
+ assert '"key": "value"' in target.innerHTML
+ value = json.loads(target.innerText)
+ assert value["key"] == "value"
+ assert value["number"] == 42
+
+
+async def test_repr_png_bytes():
+ """
+ _repr_png_ can render raw bytes.
+ """
+
+ class PNGBytes:
+ def _repr_png_(self):
+ # Valid 1x1 transparent PNG as bytes.
+ return b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\rIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82"
+
+ display(PNGBytes(), target="test-element-container", append=False)
+ target = web.page.find("#test-element-container")[0]
+ img = target.find("img")[0]
+ assert img.src.startswith("data:image/png;base64,")
+
+
+async def test_repr_png_base64():
+ """
+ _repr_png_ can render a base64-encoded string.
+ """
+
+ class PNGBase64:
+ def _repr_png_(self):
+ return "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
+
+ display(PNGBase64(), target="test-element-container", append=False)
+ target = web.page.find("#test-element-container")[0]
+ img = target.find("img")[0]
+ assert img.src.startswith("data:image/png;base64,")
+
+
+async def test_repr_jpeg():
+ """
+ Objects with _repr_jpeg_ should render as JPEG images.
+ """
+
+ class JPEGRepr:
+ def _repr_jpeg_(self):
+ # Minimal valid JPEG header (won't display but tests the path).
+ return b"\xff\xd8\xff\xe0\x00\x10JFIF"
+
+ display(JPEGRepr(), target="test-element-container", append=False)
+ target = web.page.find("#test-element-container")[0]
+ img = target.find("img")[0]
+ assert img.src.startswith("data:image/jpeg;base64,")
+
+
+async def test_repr_jpeg_base64():
+ """
+ _repr_jpeg_ can render a base64-encoded string.
+ """
+
+ class JPEGBase64:
+ def _repr_jpeg_(self):
+ return "ZCBqcGVnIG1pbmltdW0=="
+
+ display(JPEGBase64(), target="test-element-container", append=False)
+ target = web.page.find("#test-element-container")[0]
+ img = target.find("img")[0]
+ assert img.src.startswith("data:image/jpeg;base64,")
+
+
+async def test_object_with_no_repr_methods():
+ """
+ Objects with no representation methods should fall back to __repr__ with warning.
+ """
+
+ class NoReprMethods:
+ pass
+
+ obj = NoReprMethods()
+ display(obj)
+ container = await get_display_container()
+ # Should contain the default repr output - the class name. :-)
+ assert "NoReprMethods" in container.innerText
+
+
+async def test_repr_method_returns_none():
+ """
+ If a repr method exists but returns None, try next method.
+ """
+
+ class NoneReturner:
+ def _repr_html_(self):
+ return None
+
+ def __repr__(self):
+ return "Fallback repr"
+
+ display(NoneReturner())
+ container = await get_display_container()
+ assert container.innerText == "Fallback repr"
+
+
+async def test_multiple_repr_methods_priority():
+ """
+ When multiple repr methods exist, should use first available in priority order.
+ """
+
+ class MultipleReprs:
+ def _repr_html_(self):
+ # Highest priority.
+ return "