Skip to content

Commit a02ff69

Browse files
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>
1 parent 90ae3ce commit a02ff69

38 files changed

+5775
-1820
lines changed

.pre-commit-config.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ repos:
1111
hooks:
1212
- id: check-builtin-literals
1313
- id: check-case-conflict
14-
- id: check-docstring-first
1514
- id: check-executables-have-shebangs
1615
- id: check-json
1716
exclude: tsconfig\.json

core/src/stdlib/pyscript.js

Lines changed: 13 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/src/stdlib/pyscript/__init__.py

Lines changed: 100 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,105 @@
1-
# Some notes about the naming conventions and the relationship between various
2-
# similar-but-different names.
3-
#
4-
# import pyscript
5-
# this package contains the main user-facing API offered by pyscript. All
6-
# the names which are supposed be used by end users should be made
7-
# available in pyscript/__init__.py (i.e., this file)
8-
#
9-
# import _pyscript
10-
# this is an internal module implemented in JS. It is used internally by
11-
# the pyscript package, end users should not use it directly. For its
12-
# implementation, grep for `interpreter.registerJsModule("_pyscript",
13-
# ...)` in core.js
14-
#
15-
# import js
16-
# this is the JS globalThis, as exported by pyodide and/or micropython's
17-
# FFIs. As such, it contains different things in the main thread or in a
18-
# worker.
19-
#
20-
# import pyscript.magic_js
21-
# this submodule abstracts away some of the differences between the main
22-
# thread and the worker. In particular, it defines `window` and `document`
23-
# in such a way that these names work in both cases: in the main thread,
24-
# they are the "real" objects, in the worker they are proxies which work
25-
# thanks to coincident.
26-
#
27-
# from pyscript import window, document
28-
# these are just the window and document objects as defined by
29-
# pyscript.magic_js. This is the blessed way to access them from pyscript,
30-
# as it works transparently in both the main thread and worker cases.
1+
"""
2+
This is the main `pyscript` namespace. It provides the primary Pythonic API
3+
for users to interact with the
4+
[browser's own API](https://developer.mozilla.org/en-US/docs/Web/API). It
5+
includes utilities for common activities such as displaying content, handling
6+
events, fetching resources, managing local storage, and coordinating with
7+
web workers.
8+
9+
The most important names provided by this namespace can be directly imported
10+
from `pyscript`, for example:
11+
12+
```python
13+
from pyscript import display, HTML, fetch, when, storage, WebSocket
14+
```
15+
16+
The following names are available in the `pyscript` namespace:
17+
18+
- `RUNNING_IN_WORKER`: Boolean indicating if the code is running in a Web
19+
Worker.
20+
- `PyWorker`: Class for creating Web Workers running Python code.
21+
- `config`: Configuration object for pyscript settings.
22+
- `current_target`: The element in the DOM that is the current target for
23+
output.
24+
- `document`: The standard `document` object, proxied in workers.
25+
- `window`: The standard `window` object, proxied in workers.
26+
- `js_import`: Function to dynamically import JS modules.
27+
- `js_modules`: Object containing JS modules available to Python.
28+
- `sync`: Utility for synchronizing between worker and main thread.
29+
- `display`: Function to render Python objects in the web page.
30+
- `HTML`: Helper class to create HTML content for display.
31+
- `fetch`: Function to perform HTTP requests.
32+
- `Storage`: Class representing browser storage (local/session).
33+
- `storage`: Object to interact with browser's local storage.
34+
- `WebSocket`: Class to create and manage WebSocket connections.
35+
- `when`: Function to register event handlers on DOM elements.
36+
- `Event`: Class representing user defined or DOM events.
37+
- `py_import`: Function to lazily import Pyodide related Python modules.
38+
39+
If running in the main thread, the following additional names are available:
40+
41+
- `create_named_worker`: Function to create a named Web Worker.
42+
- `workers`: Object to manage and interact with existing Web Workers.
43+
44+
All of these names are defined in the various submodules of `pyscript` and
45+
are imported and re-exported here for convenience. Please refer to the
46+
respective submodule documentation for more details on each component.
47+
48+
49+
!!! Note
50+
Some notes about the naming conventions and the relationship between
51+
various similar-but-different names found within this code base.
52+
53+
```python
54+
import pyscript
55+
```
56+
57+
The `pyscript` package contains the main user-facing API offered by
58+
PyScript. All the names which are supposed be used by end users should
59+
be made available in `pyscript/__init__.py` (i.e., this source file).
60+
61+
```python
62+
import _pyscript
63+
```
64+
65+
The `_pyscript` module is an internal API implemented in JS. **End users
66+
should not use it directly**. For its implementation, grep for
67+
`interpreter.registerJsModule("_pyscript",...)` in `core.js`.
68+
69+
```python
70+
import js
71+
```
72+
73+
The `js` object is
74+
[the JS `globalThis`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis),
75+
as exported by Pyodide and/or Micropython's foreign function interface
76+
(FFI). As such, it contains different things in the main thread or in a
77+
worker, as defined by web standards.
78+
79+
```python
80+
import pyscript.context
81+
```
82+
83+
The `context` submodule abstracts away some of the differences between
84+
the main thread and a worker. Its most important features are made
85+
available in the root `pyscript` namespace. All other functionality is
86+
mostly for internal PyScript use or advanced users. In particular, it
87+
defines `window` and `document` in such a way that these names work in
88+
both cases: in the main thread, they are the "real" objects, in a worker
89+
they are proxies which work thanks to
90+
[coincident](https://github.com/WebReflection/coincident).
91+
92+
```python
93+
from pyscript import window, document
94+
```
95+
96+
These are just the `window` and `document` objects as defined by
97+
`pyscript.context`. This is the blessed way to access them from `pyscript`,
98+
as it works transparently in both the main thread and worker cases.
99+
"""
31100

32101
from polyscript import lazy_py_modules as py_import
33-
from pyscript.magic_js import (
102+
from pyscript.context import (
34103
RUNNING_IN_WORKER,
35104
PyWorker,
36105
config,
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
"""
2+
Execution context management for PyScript.
3+
4+
This module handles the differences between running in the
5+
[main browser thread](https://developer.mozilla.org/en-US/docs/Glossary/Main_thread)
6+
versus running in a
7+
[Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers),
8+
providing a consistent API regardless of the execution context.
9+
10+
Key features:
11+
12+
- Detects whether code is running in a worker or main thread. Read this via
13+
the boolean `pyscript.context.RUNNING_IN_WORKER`.
14+
- Parses and normalizes configuration from `polyscript.config` and adds the
15+
Python interpreter type via the `type` key in `pyscript.context.config`.
16+
- Provides appropriate implementations of `window`, `document`, and `sync`.
17+
- Sets up JavaScript module import system, including a lazy `js_import`
18+
function.
19+
- Manages `PyWorker` creation.
20+
- Provides access to the current display target via
21+
`pyscript.context.display_target`.
22+
23+
!!! warning
24+
25+
These are key differences between the main thread and worker contexts:
26+
27+
Main thread context:
28+
29+
- `window` and `document` are available directly.
30+
- `PyWorker` can be created to spawn worker threads.
31+
- `sync` is not available (raises `NotSupported`).
32+
33+
Worker context:
34+
35+
- `window` and `document` are proxied from main thread (if SharedArrayBuffer
36+
available).
37+
- `PyWorker` is not available (raises `NotSupported`).
38+
- `sync` utilities are available for main thread communication.
39+
"""
40+
41+
import json
42+
import sys
43+
44+
import js
45+
from polyscript import config as _polyscript_config
46+
from polyscript import js_modules
47+
from pyscript.util import NotSupported
48+
49+
RUNNING_IN_WORKER = not hasattr(js, "document")
50+
"""Detect execution context: True if running in a worker, False if main thread."""
51+
52+
config = json.loads(js.JSON.stringify(_polyscript_config))
53+
"""Parsed and normalized configuration."""
54+
if isinstance(config, str):
55+
config = {}
56+
57+
js_import = None
58+
"""Function to import JavaScript modules dynamically."""
59+
60+
window = None
61+
"""The `window` object (proxied if in a worker)."""
62+
63+
document = None
64+
"""The `document` object (proxied if in a worker)."""
65+
66+
sync = None
67+
"""Sync utilities for worker-main thread communication (only in workers)."""
68+
69+
# Detect and add Python interpreter type to config.
70+
if "MicroPython" in sys.version:
71+
config["type"] = "mpy"
72+
else:
73+
config["type"] = "py"
74+
75+
76+
class _JSModuleProxy:
77+
"""
78+
Proxy for JavaScript modules imported via js_modules.
79+
80+
This allows Python code to import JavaScript modules using Python's
81+
import syntax:
82+
83+
```python
84+
from pyscript.js_modules lodash import debounce
85+
```
86+
87+
The proxy lazily retrieves the actual JavaScript module when accessed.
88+
"""
89+
90+
def __init__(self, name):
91+
"""
92+
Create a proxy for the named JavaScript module.
93+
"""
94+
self.name = name
95+
96+
def __getattr__(self, field):
97+
"""
98+
Retrieve a JavaScript object/function from the proxied JavaScript
99+
module via the given `field` name.
100+
"""
101+
# Avoid Pyodide looking for non-existent special methods.
102+
if not field.startswith("_"):
103+
return getattr(getattr(js_modules, self.name), field)
104+
return None
105+
106+
107+
# Register all available JavaScript modules in Python's module system.
108+
# This enables: from pyscript.js_modules.xxx import yyy
109+
for module_name in js.Reflect.ownKeys(js_modules):
110+
sys.modules[f"pyscript.js_modules.{module_name}"] = _JSModuleProxy(module_name)
111+
sys.modules["pyscript.js_modules"] = js_modules
112+
113+
114+
# Context-specific setup: Worker vs Main Thread.
115+
if RUNNING_IN_WORKER:
116+
import polyscript
117+
118+
# PyWorker cannot be created from within a worker.
119+
PyWorker = NotSupported(
120+
"pyscript.PyWorker",
121+
"pyscript.PyWorker works only when running in the main thread",
122+
)
123+
124+
# Attempt to access main thread's window and document via SharedArrayBuffer.
125+
try:
126+
window = polyscript.xworker.window
127+
document = window.document
128+
js.document = document
129+
130+
# Create js_import function that runs imports on the main thread.
131+
js_import = window.Function(
132+
"return (...urls) => Promise.all(urls.map((url) => import(url)))"
133+
)()
134+
135+
except:
136+
# SharedArrayBuffer not available - window/document cannot be proxied.
137+
sab_error_message = (
138+
"Unable to use `window` or `document` in worker. "
139+
"This requires SharedArrayBuffer support. "
140+
"See: https://docs.pyscript.net/latest/faq/#sharedarraybuffer"
141+
)
142+
js.console.warn(sab_error_message)
143+
window = NotSupported("pyscript.window", sab_error_message)
144+
document = NotSupported("pyscript.document", sab_error_message)
145+
146+
# Worker-specific utilities for main thread communication.
147+
sync = polyscript.xworker.sync
148+
149+
def current_target():
150+
"""
151+
Get the current output target in worker context.
152+
"""
153+
return polyscript.target
154+
155+
else:
156+
# Main thread context setup.
157+
import _pyscript
158+
from _pyscript import PyWorker as _PyWorker
159+
from pyscript.ffi import to_js
160+
161+
js_import = _pyscript.js_import
162+
163+
def PyWorker(url, **options):
164+
"""
165+
Create a Web Worker running Python code.
166+
167+
This spawns a new worker thread that can execute Python code
168+
found at the `url`, independently of the main thread. The
169+
`**options` can be used to configure the worker.
170+
171+
```python
172+
from pyscript import PyWorker
173+
174+
175+
# Create a worker to run background tasks.
176+
# (`type` MUST be either `micropython` or `pyodide`)
177+
worker = PyWorker("./worker.py", type="micropython")
178+
```
179+
180+
PyWorker **can only be created from the main thread**, not from
181+
within another worker.
182+
"""
183+
return _PyWorker(url, to_js(options))
184+
185+
# Main thread has direct access to window and document.
186+
window = js
187+
document = js.document
188+
189+
# sync is not available in main thread (only in workers).
190+
sync = NotSupported(
191+
"pyscript.sync", "pyscript.sync works only when running in a worker"
192+
)
193+
194+
def current_target():
195+
"""
196+
Get the current output target in main thread context.
197+
"""
198+
return _pyscript.target

0 commit comments

Comments
 (0)