8000 Refactor @when and add Event (#2239) · pyscript/pyscript@56c64cb · GitHub
[go: up one dir, main page]

Skip to content

Commit 56c64cb

Browse files
ntollWebReflection
andauthored
Refactor @when and add Event (#2239)
* Add two unit tests for illustrative purposes. * Radical simplification of @when, more tests and some minor refactoring. Handle ElementCollections, tests for ElementCollection, make serve for running tests locally. * Skip flakey Pyodide in worker test (it works 50/50 and appears to be a timing issue). * Ensure onFOO relates to an underlying FOO event in an Element. * Minor comment cleanup. * Add async test for Event listeners. * Handlers no longer require an event parameter. * Add tests for async handling via when. * Docstring cleanup. * Refactor onFOO to on_FOO. * Minor typo tidy ups. * Use correct check for MicroPython. --------- Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
1 parent 4ff02a2 commit 56c64cb

File tree

16 files changed

+749
-357
lines changed

16 files changed

+749
-357
lines changed

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ precommit-check:
7070
test:
7171
cd core && npm run test:integration
7272

73+
# Serve the repository with the correct headers.
74+
serve:
75+
npx mini-coi .
76+
7377
# Format the code.
7478
fmt: fmt-py
7579
@echo "Format completed"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ Read the [contributing guide](https://docs.pyscript.net/latest/contributing/)
7676
to learn about our development process, reporting bugs and improvements,
7777
creating issues and asking questions.
7878

79-
Check out the [developing process](https://docs.pyscript.net/latest/developers/)
79+
Check out the [development process](https://docs.pyscript.net/latest/developers/)
8080
documentation for more information on how to setup your development environment.
8181

8282
## Governance

core/src/stdlib/pyscript.js

Lines changed: 4 additions & 4 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: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@
3030
# as it works transparently in both the main thread and worker cases.
3131

3232
from polyscript import lazy_py_modules as py_import
33-
from pyscript.display import HTML, display
34-
from pyscript.fetch import fetch
3533
from pyscript.magic_js import (
3634
RUNNING_IN_WORKER,
3735
PyWorker,
@@ -43,19 +41,11 @@
4341
sync,
4442
window,
4543
)
44+
from pyscript.display import HTML, display
45+
from pyscript.fetch import fetch
4646
from pyscript.storage import Storage, storage
4747
from pyscript.websocket import WebSocket
48+
from pyscript.events import when, Event
4849

4950
if not RUNNING_IN_WORKER:
5051
from pyscript.workers import create_named_worker, workers
51-
52-
try:
53-
from pyscript.event_handling import when
54-
except:
55-
# TODO: should we remove this? Or at the very least, we should capture
56-
# the traceback otherwise it's very hard to debug
57-
from pyscript.util import NotSupported
58-
59-
when = NotSupported(
60-
"pyscript.when", "pyscript.when currently not available with this interpreter"
61-
)

core/src/stdlib/pyscript/event_handling.py

Lines changed: 0 additions & 76 deletions
This file was deleted.

core/src/stdlib/pyscript/events.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import asyncio
2+
import inspect
3+
import sys
4+
5+
from functools import wraps
6+
from pyscript.magic_js import document
7+
from pyscript.ffi import create_proxy
8+
from pyscript.util import is_awaitable
9+
from pyscript import config
10+
11+
12+
class Event:
13+
"""
14+
Represents something that may happen at some point in the future.
15+
"""
16+
17+
def __init__(self):
18+
self._listeners = []
19+
20+
def trigger(self, result):
21+
"""
22+
Trigger the event with a result to pass into the handlers.
23+
"""
24+
for listener in self._listeners:
25+
if is_awaitable(listener):
26+
# Use create task to avoid making this an async function.
27+
asyncio.create_task(listener(result))
28+
else:
29+
listener(result)
30+
31+
def add_listener(self, listener):
32+
"""
33+
Add a callable/awaitable to listen to when this event is triggered.
34+
"""
35+
if is_awaitable(listener) or callable(listener):
36+
if listener not in self._listeners:
37+
self._listeners.append(listener)
38+
else:
39+
raise ValueError("Listener must be callable or awaitable.")
40+
41+
def remove_listener(self, *args):
42+
"""
43+
Clear the specified handler functions in *args. If no handlers
44+
provided, clear all handlers.
45+
"""
46+
if args:
47+
for listener in args:
48+
self._listeners.remove(listener)
49+
else:
50+
self._listeners = []
51+
52+
53+
def when(target, *args, **kwargs):
54+
"""
55+
Add an event listener to the target element(s) for the specified event type.
56+
57+
The target can be a string representing the event type, or an Event object.
58+
If the target is an Event object, the event listener will be added to that
59+
object. If the target is a string, the event listener will be added to the
60+
element(s) that match the (second) selector argument.
61+
62+
If a (third) handler argument is provided, it will be called when the event
63+
is triggered; thus allowing this to be used as both a function and a
64+
decorator.
65+
"""
66+
# If "when" is called as a function, try to grab the handler from the
67+
# arguments. If there's no handler, this must be a decorator based call.
68+
handler = None
69+
if args and (callable(args[0]) or is_awaitable(args[0])):
70+
handler = args[0]
71+
elif callable(kwargs.get("handler")) or is_awaitable(kwargs.get("handler")):
72+
handler = kwargs.pop("handler")
73+
# If the target is a string, it is the "older" use of `when` where it
74+
# represents the name of a DOM event.
75+
if isinstance(target, str):
76+
# Extract the selector from the arguments or keyword arguments.
77+
selector = args[0] if args else kwargs.pop("selector")
78+
if not selector:
79+
raise ValueError("No selector provided.")
80+
# Grab the DOM elements to which the target event will be attached.
81+
from pyscript.web import Element, ElementCollection
82+
83+
if isinstance(selector, str):
84+
elements = document.querySelectorAll(selector)
85+
elif isinstance(selector, Element):
86+
elements = [selector._dom_element]
87+
elif isinstance(selector, ElementCollection):
88+
elements = [el._dom_element for el in selector]
89+
else:
90+
elements = selector if isinstance(selector, list) else [selector]
91+
92+
def decorator(func):
93+
if config["type"] == "mpy": # Is MicroPython?
94+
if is_awaitable(func):
95+
96+
async def wrapper(*args, **kwargs):
97+
"""
98+
This is a very ugly hack to get micropython working because
99+
`inspect.signature` doesn't exist. It may be actually better
100+
to not try any magic for now and raise the error.
101+
"""
102+
try:
103+
return await func(*args, **kwargs)
104+
105+
except TypeError as e:
106+
if "takes" in str(e) and "positional arguments" in str(e):
107+
return await func()
108+
raise
109+
110+
else:
111+
112+
def wrapper(*args, **kwargs):
113+
"""
114+
This is a very ugly hack to get micropython working because
115+
`inspect.signature` doesn't exist. It may be actually better
116+
to not try any magic for now and raise the error.
117+
"""
118+
try:
119+
return func(*args, **kwargs)
120+
121+
except TypeError as e:
122+
if "takes" in str(e) and "positional arguments" in str(e):
123+
return func()
124+
raise
125+
126+
else:
127+
sig = inspect.signature(func)
128+
if sig.parameters:
129+
if is_awaitable(func):
130+
131+
async def wrapper(event):
132+
return await func(event)
133+
134+
else:
135+
wrapper = func
136+
else:
137+
# Function doesn't receive events.
138+
if is_awaitable(func):
139+
140+
async def wrapper(*args, **kwargs):
141+
return await func()
142+
143+
else:
144+
145+
def wrapper(*args, **kwargs):
146+
return func()
147+
148+
wrapper = wraps(func)(wrapper)
149+
if isinstance(target, Event):
150+
# The target is a single Event object.
151+
target.add_listener(wrapper)
152+
elif isinstance(target, list) and all(isinstance(t, Event) for t in target):
153+
# The target is a list of Event objects.
154+
for evt in target:
155+
evt.add_listener(wrapper)
156+
else:
157+
# The target is a string representing an event type, and so a
158+
# DOM element or collection of elements is found in "elements".
159+
for el in elements:
160+
el.addEventListener(target, create_proxy(wrapper))
161+
return wrapper
162+
163+
# If "when" was called as a decorator, return the decorator function,
164+
# otherwise just call the internal decorator function with the supplied
165+
# handler.
166+
return decorator(handler) if handler else decorator

core/src/stdlib/pyscript/util.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import js
2+
import sys
3+
import inspect
24

35

46
def as_bytearray(buffer):
7+
"""
8+
Given a JavaScript ArrayBuffer, convert it to a Python bytearray in a
9+
MicroPython friendly manner.
10+
"""
511
ui8a = js.Uint8Array.new(buffer)
612
size = ui8a.length
713
ba = bytearray(size)
@@ -31,3 +37,22 @@ def __setattr__(self, attr, value):
3137

3238
def __call__(self, *args):
3339
raise TypeError(self.error)
40+
41+
42+
def is_awaitable(obj):
43+
"""
44+
Returns a boolean indication if the passed in obj is an awaitable
45+
function. (MicroPython treats awaitables as generator functions, and if
46+
the object is a closure containing an async function we need to work
47+
carefully.)
48+
"""
49+
from pyscript import config
50+
51+
if config["type"] == "mpy": # Is MicroPython?
52+
# MicroPython doesn't appear to have a way to determine if a closure is
53+
# an async function except via the repr. This is a bit hacky.
54+
if "<closure <generator>" in repr(obj):
55+
return True
56+
return inspect.isgeneratorfunction(obj)
57+
58+
return inspect.iscoroutinefunction(obj)

0 commit comments

Comments
 (0)
0