diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 70fe6fb515b..43d9ea56cc7 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 fcac6257044..840212eb222 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
+
+
+# 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.
+ """
-# this solves an inconsistency between Pyodide and MicroPython
-# @see https://github.com/pyscript/pyscript/issues/2106
-class _ReadOnlyProxy:
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.
-workers = _ReadOnlyProxy()
+ ```python
+ worker = await workers.my_worker
+ ```
+ """
+ return js.Reflect.get(_polyscript_workers, name)
-async def create_named_worker(src="", name="", config=None, type="py"):
- from json import dumps
+# Global workers proxy for accessing named workers.
+workers = _ReadOnlyWorkersProxy()
+"""Global proxy for accessing named web workers."""
- if not src:
- msg = "Named workers require src"
- raise ValueError(msg)
- if not name:
- msg = "Named workers require a name"
- raise ValueError(msg)
+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`).
- s = _js.document.createElement("script")
- s.type = type
- s.src = src
- _set(s, "worker")
- _set(s, "name", name)
+ This function creates a new web worker by injecting a `
+
", {"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 "