mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 10:17:23 -05:00
A more refined Event class, with additional tests.
This commit is contained in:
@@ -3,7 +3,7 @@ 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": "_L='_repr_mimebundle_'\n_K='image/svg+xml'\n_J='application/json'\n_I='__repr__'\n_H='savefig'\n_G='text/html'\n_F='image/jpeg'\n_E='application/javascript'\n_D='utf-8'\n_C='text/plain'\n_B='image/png'\n_A=None\nimport base64,html,io,re\nfrom pyscript.magic_js import current_target,document,window\n_MIME_METHODS={_H:_B,'_repr_javascript_':_E,'_repr_json_':_J,'_repr_latex':'text/latex','_repr_png_':_B,'_repr_jpeg_':_F,'_repr_pdf_':'application/pdf','_repr_svg_':_K,'_repr_markdown_':'text/markdown','_repr_html_':_G,_I:_C}\ndef _render_image(mime,value,meta):\n\tA=value\n\tif isinstance(A,bytes):A=base64.b64encode(A).decode(_D)\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(_D)).decode(_D)\n\tC=f\"data:{mime};charset=utf-8;base64,{A}\";D=' '.join(['{k}=\"{v}\"'for(A,B)in meta.items()]);return f'<img src=\"{C}\" {D}></img>'\ndef _identity(value,meta):return value\n_MIME_RENDERERS={_C:html.escape,_G:_identity,_B:lambda value,meta:_render_image(_B,value,meta),_F:lambda value,meta:_render_image(_F,value,meta),_K:_identity,_J:_identity,_E:lambda value,meta:f\"<script>{value}<\\\\/script>\"}\nclass HTML:\n\tdef __init__(A,html):A._html=html\n\tdef _repr_html_(A):return A._html\ndef _eval_formatter(obj,print_method):\n\tB=obj;A=print_method\n\tif A==_I:return repr(B)\n\tif hasattr(B,A):\n\t\tif A==_H:C=io.BytesIO();B.savefig(C,format='png');C.seek(0);return base64.b64encode(C.read()).decode(_D)\n\t\treturn getattr(B,A)()\n\tif A==_L:return{},{}\ndef _format_mime(obj):\n\tC=obj\n\tif isinstance(C,str):return html.escape(C),_C\n\tD=_eval_formatter(C,_L)\n\tif isinstance(D,tuple):E,I=D\n\telse:E=D\n\tA,F=_A,[]\n\tfor(H,B)in _MIME_METHODS.items():\n\t\tif B in E:A=E[B]\n\t\telse:A=_eval_formatter(C,H)\n\t\tif A is _A:continue\n\t\tif B not in _MIME_RENDERERS:F.append(B);continue\n\t\tbreak\n\tif A is _A:\n\t\tif F:window.console.warn(f\"Rendered object requested unavailable MIME renderers: {F}\")\n\t\tA=repr(A);B=_C\n\telif isinstance(A,tuple):A,G=A\n\telse:G={}\n\treturn _MIME_RENDERERS[B](A,G),B\ndef _write(element,value,append=False):\n\tB=element;C,D=_format_mime(value)\n\tif C=='\\\\n':return\n\tif append:A=document.createElement('div');B.append(A)\n\telse:\n\t\tA=B.lastElementChild\n\t\tif A is _A:A=B\n\tif D in(_E,_G):E=document.createRange().createContextualFragment(C);A.append(E)\n\telse:A.innerHTML=C\ndef display(*E,target=_A,append=True):\n\tD=append;A=target\n\tif A is _A:A=current_target()\n\telif not isinstance(A,str):C=f\"target must be str or None, not {A.__class__.__name__}\";raise TypeError(C)\n\telif A=='':C='Cannot have an empty target';raise ValueError(C)\n\telif A.startswith('#'):A=A[1:]\n\tB=document.getElementById(A)\n\tif B is _A:C=f\"Invalid selector with id={A}. Cannot be found in the page.\";raise ValueError(C)\n\tif B.tagName=='SCRIPT'and hasattr(B,'target'):B=B.target\n\tfor F in E:\n\t\tif not D:B.replaceChildren()\n\t\t_write(B,F,append=D)",
|
||||
"events.py": "import asyncio,inspect,sys\nfrom functools import wraps\nfrom pyscript.magic_js import document\nfrom pyscript.ffi import create_proxy\nfrom pyscript.util import is_awaitable\nfrom pyscript import config\nclass Event:\n\tdef __init__(A):A._listeners=[]\n\tdef trigger(C,result):\n\t\tB=result\n\t\tfor A in C._listeners:\n\t\t\tif is_awaitable(A):asyncio.create_task(A(B))\n\t\t\telse:A(B)\n\tdef add_listener(B,listener):\n\t\tA=listener\n\t\tif is_awaitable(A)or callable(A):\n\t\t\tif A not in B._listeners:B._listeners.append(A)\n\t\telse:C='Listener must be callable or awaitable.';raise ValueError(C)\n\tdef remove_listener(A,*B):\n\t\tif B:\n\t\t\tfor C in B:A._listeners.remove(C)\n\t\telse:A._listeners=[]\ndef when(target,*B,**D):\n\tG='handler';C=target;E=None\n\tif B and(callable(B[0])or is_awaitable(B[0])):E=B[0]\n\telif callable(D.get(G))or is_awaitable(D.get(G)):E=D.pop(G)\n\tif isinstance(C,str):\n\t\tA=B[0]if B else D.pop('selector')\n\t\tif not A:I='No selector provided.';raise ValueError(I)\n\t\tfrom pyscript.web import Element as J,ElementCollection as K\n\t\tif isinstance(A,str):F=document.querySelectorAll(A)\n\t\telif isinstance(A,J):F=[A._dom_element]\n\t\telif isinstance(A,K):F=[A._dom_element for A in A]\n\t\telse:F=A if isinstance(A,list)else[A]\n\tdef H(func):\n\t\tE='positional arguments';D='takes';A=func\n\t\tif config['type']=='mpy':\n\t\t\tif is_awaitable(A):\n\t\t\t\tasync def B(*C,**F):\n\t\t\t\t\ttry:return await A(*C,**F)\n\t\t\t\t\texcept TypeError as B:\n\t\t\t\t\t\tif D in str(B)and E in str(B):return await A()\n\t\t\t\t\t\traise\n\t\t\telse:\n\t\t\t\tdef B(*C,**F):\n\t\t\t\t\ttry:return A(*C,**F)\n\t\t\t\t\texcept TypeError as B:\n\t\t\t\t\t\tif D in str(B)and E in str(B):return A()\n\t\t\t\t\t\traise\n\t\telse:\n\t\t\tG=inspect.signature(A)\n\t\t\tif G.parameters:\n\t\t\t\tif is_awaitable(A):\n\t\t\t\t\tasync def B(event):return await A(event)\n\t\t\t\telse:B=A\n\t\t\telif is_awaitable(A):\n\t\t\t\tasync def B(*B,**C):return await A()\n\t\t\telse:\n\t\t\t\tdef B(*B,**C):return A()\n\t\tB=wraps(A)(B)\n\t\tif isinstance(C,Event):C.add_listener(B)\n\t\telif isinstance(C,list)and all(isinstance(A,Event)for A in C):\n\t\t\tfor H in C:H.add_listener(B)\n\t\telse:\n\t\t\tfor I in F:I.addEventListener(C,create_proxy(B))\n\t\treturn B\n\treturn H(E)if E else H",
|
||||
"events.py": "import asyncio,inspect\nfrom functools import wraps\nfrom pyscript.magic_js import document\nfrom pyscript.ffi import create_proxy\nfrom pyscript.util import is_awaitable\nfrom pyscript import config\nclass Event:\n\tdef __init__(A):A._listeners=[];A._result=None;A._triggered=False\n\t@property\n\tdef triggered(self):return self._triggered\n\t@property\n\tdef result(self):\n\t\tif self.triggered:return self._result\n\t\tA='Event has not been triggered yet. No result available.';raise ValueError(A)\n\tdef trigger(A,result=None):\n\t\tif A.triggered:B='Event has already been triggered.';raise RuntimeError(B)\n\t\tA._triggered=True;A._result=result\n\t\tfor C in A._listeners:A._call_listener(C)\n\tdef add_listener(B,listener):\n\t\tA=listener\n\t\tif is_awaitable(A)or callable(A):\n\t\t\tif A not in B._listeners:\n\t\t\t\tB._listeners.append(A)\n\t\t\t\tif B.triggered:B._call_listener(A)\n\t\telse:C='Listener must be callable or awaitable.';raise ValueError(C)\n\tdef remove_listener(A,*B):\n\t\tif B:\n\t\t\tfor C in B:A._listeners.remove(C)\n\t\telse:A._listeners=[]\n\tdef _call_listener(B,listener):\n\t\tA=listener\n\t\tif is_awaitable(A):asyncio.create_task(A(B._result))\n\t\telse:A(B._result)\ndef when(target,*B,**D):\n\tG='handler';C=target;E=None\n\tif B and(callable(B[0])or is_awaitable(B[0])):E=B[0]\n\telif callable(D.get(G))or is_awaitable(D.get(G)):E=D.pop(G)\n\tif isinstance(C,str):\n\t\tA=B[0]if B else D.pop('selector')\n\t\tif not A:I='No selector provided.';raise ValueError(I)\n\t\tfrom pyscript.web import Element as J,ElementCollection as K\n\t\tif isinstance(A,str):F=document.querySelectorAll(A)\n\t\telif isinstance(A,J):F=[A._dom_element]\n\t\telif isinstance(A,K):F=[A._dom_element for A in A]\n\t\telse:F=A if isinstance(A,list)else[A]\n\tdef H(func):\n\t\tE='positional arguments';D='takes';A=func\n\t\tif config['type']=='mpy':\n\t\t\tif is_awaitable(A):\n\t\t\t\tasync def B(*C,**F):\n\t\t\t\t\ttry:return await A(*C,**F)\n\t\t\t\t\texcept TypeError as B:\n\t\t\t\t\t\tif D in str(B)and E in str(B):return await A()\n\t\t\t\t\t\traise\n\t\t\telse:\n\t\t\t\tdef B(*C,**F):\n\t\t\t\t\ttry:return A(*C,**F)\n\t\t\t\t\texcept TypeError as B:\n\t\t\t\t\t\tif D in str(B)and E in str(B):return A()\n\t\t\t\t\t\traise\n\t\telse:\n\t\t\tG=inspect.signature(A)\n\t\t\tif G.parameters:\n\t\t\t\tif is_awaitable(A):\n\t\t\t\t\tasync def B(event):return await A(event)\n\t\t\t\telse:B=A\n\t\t\telif is_awaitable(A):\n\t\t\t\tasync def B(*B,**C):return await A()\n\t\t\telse:\n\t\t\t\tdef B(*B,**C):return A()\n\t\tB=wraps(A)(B)\n\t\tif isinstance(C,Event):C.add_listener(B)\n\t\telif isinstance(C,list)and all(isinstance(A,Event)for A in C):\n\t\t\tfor H in C:H.add_listener(B)\n\t\telse:\n\t\t\tfor I in F:I.addEventListener(C,create_proxy(B))\n\t\treturn B\n\treturn H(E)if E else H",
|
||||
"fetch.py": "import json,js\nfrom pyscript.util import as_bytearray\nclass _Response:\n\tdef __init__(A,response):A._response=response\n\tdef __getattr__(A,attr):return getattr(A._response,attr)\n\tasync def arrayBuffer(B):\n\t\tA=await B._response.arrayBuffer()\n\t\tif hasattr(A,'to_py'):return A.to_py()\n\t\treturn memoryview(as_bytearray(A))\n\tasync def blob(A):return await A._response.blob()\n\tasync def bytearray(A):B=await A._response.arrayBuffer();return as_bytearray(B)\n\tasync def json(A):return json.loads(await A.text())\n\tasync def text(A):return await A._response.text()\nclass _DirectResponse:\n\t@staticmethod\n\tdef setup(promise,response):A=promise;A._response=_Response(response);return A._response\n\tdef __init__(B,promise):A=promise;B._promise=A;A._response=None;A.arrayBuffer=B.arrayBuffer;A.blob=B.blob;A.bytearray=B.bytearray;A.json=B.json;A.text=B.text\n\tasync def _response(A):\n\t\tif not A._promise._response:await A._promise\n\t\treturn A._promise._response\n\tasync def arrayBuffer(A):B=await A._response();return await B.arrayBuffer()\n\tasync def blob(A):B=await A._response();return await B.blob()\n\tasync def bytearray(A):B=await A._response();return await B.bytearray()\n\tasync def json(A):B=await A._response();return await B.json()\n\tasync def text(A):B=await A._response();return await B.text()\ndef fetch(url,**B):C=js.JSON.parse(json.dumps(B));D=lambda response,*B:_DirectResponse.setup(A,response);A=js.fetch(url,C).then(D);_DirectResponse(A);return A",
|
||||
"ffi.py": "try:\n\timport js;from pyodide.ffi import create_proxy as _cp,to_js as _py_tjs;from_entries=js.Object.fromEntries\n\tdef _tjs(value,**A):\n\t\tB='dict_converter'\n\t\tif not hasattr(A,B):A[B]=from_entries\n\t\treturn _py_tjs(value,**A)\nexcept:from jsffi import create_proxy as _cp;from jsffi import to_js as _tjs\ncreate_proxy=_cp\nto_js=_tjs",
|
||||
"flatted.py": "import json as _json\nclass _Known:\n\tdef __init__(A):A.key=[];A.value=[]\nclass _String:\n\tdef __init__(A,value):A.value=value\ndef _array_keys(value):\n\tA=[];B=0\n\tfor C in value:A.append(B);B+=1\n\treturn A\ndef _object_keys(value):\n\tA=[]\n\tfor B in value:A.append(B)\n\treturn A\ndef _is_array(value):return isinstance(value,(list,tuple))\ndef _is_object(value):return isinstance(value,dict)\ndef _is_string(value):return isinstance(value,str)\ndef _index(known,input,value):B=value;A=known;input.append(B);C=str(len(input)-1);A.key.append(B);A.value.append(C);return C\ndef _loop(keys,input,known,output):\n\tA=output\n\tfor B in keys:\n\t\tC=A[B]\n\t\tif isinstance(C,_String):_ref(B,input[int(C.value)],input,known,A)\n\treturn A\ndef _ref(key,value,input,known,output):\n\tB=known;A=value\n\tif _is_array(A)and A not in B:B.append(A);A=_loop(_array_keys(A),input,B,A)\n\telif _is_object(A)and A not in B:B.append(A);A=_loop(_object_keys(A),input,B,A)\n\toutput[key]=A\ndef _relate(known,input,value):\n\tB=known;A=value\n\tif _is_string(A)or _is_array(A)or _is_object(A):\n\t\ttry:return B.value[B.key.index(A)]\n\t\texcept:return _index(B,input,A)\n\treturn A\ndef _transform(known,input,value):\n\tB=known;A=value\n\tif _is_array(A):\n\t\tC=[]\n\t\tfor F in A:C.append(_relate(B,input,F))\n\t\treturn C\n\tif _is_object(A):\n\t\tD={}\n\t\tfor E in A:D[E]=_relate(B,input,A[E])\n\t\treturn D\n\treturn A\ndef _wrap(value):\n\tA=value\n\tif _is_string(A):return _String(A)\n\tif _is_array(A):\n\t\tB=0\n\t\tfor D in A:A[B]=_wrap(D);B+=1\n\telif _is_object(A):\n\t\tfor C in A:A[C]=_wrap(A[C])\n\treturn A\ndef parse(value,*C,**D):\n\tA=value;E=_json.loads(A,*C,**D);B=[]\n\tfor A in E:B.append(_wrap(A))\n\tinput=[]\n\tfor A in B:\n\t\tif isinstance(A,_String):input.append(A.value)\n\t\telse:input.append(A)\n\tA=input[0]\n\tif _is_array(A):return _loop(_array_keys(A),input,[A],A)\n\tif _is_object(A):return _loop(_object_keys(A),input,[A],A)\n\treturn A\ndef stringify(value,*D,**E):\n\tB=_Known();input=[];C=[];A=int(_index(B,input,value))\n\twhile A<len(input):C.append(_transform(B,input,input[A]));A+=1\n\treturn _json.dumps(C,*D,**E)",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
from functools import wraps
|
||||
from pyscript.magic_js import document
|
||||
@@ -11,38 +10,87 @@ from pyscript import config
|
||||
|
||||
class Event:
|
||||
"""
|
||||
Represents something that may happen at some point in the future.
|
||||
Events represent something that may happen at some point in time (usually
|
||||
the future). They're used to coordinate code when the timing of an event is
|
||||
not known in advance (e.g. a button click or a network response).
|
||||
|
||||
An event is triggered with an arbitrary result. If no result is given, then
|
||||
None is assumed as the result.
|
||||
|
||||
Add listener functions to the event, to be called with the result when the
|
||||
event is triggered. The listener functions can be callable or awaitable. If
|
||||
the listener is added several times, it will be called only once.
|
||||
|
||||
If the event was triggered before a listener is added, the listener will be
|
||||
called as soon as it is added, with the result of the event.
|
||||
|
||||
If the event is never triggered, then its listeners will never be called.
|
||||
It's also possible to remove listeners from the event.
|
||||
|
||||
If the result of the event is not available, a ValueError will be raised
|
||||
when trying to access the result property. A RuntimeError will be raised if
|
||||
the event is triggered more than once.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# To contain the listeners to be called when the event is triggered.
|
||||
self._listeners = []
|
||||
# The result associated with the event.
|
||||
self._result = None
|
||||
# A flag to indicate if the event has been triggered.
|
||||
self._triggered = False
|
||||
|
||||
def trigger(self, result):
|
||||
@property
|
||||
def triggered(self):
|
||||
"""
|
||||
Trigger the event with a result to pass into the handlers.
|
||||
A boolean flag to indicate if the event has been triggered.
|
||||
"""
|
||||
return self._triggered
|
||||
|
||||
@property
|
||||
def result(self):
|
||||
"""
|
||||
The result of the event.
|
||||
"""
|
||||
if self.triggered:
|
||||
return self._result
|
||||
msg = "Event has not been triggered yet. No result available."
|
||||
raise ValueError(msg)
|
||||
|
||||
def trigger(self, result=None):
|
||||
"""
|
||||
Trigger the event with an arbitrary result to pass into the listeners.
|
||||
An event may only be triggered once (otherwise a RuntimeError is
|
||||
raised).
|
||||
"""
|
||||
if self.triggered:
|
||||
msg = "Event has already been triggered."
|
||||
raise RuntimeError(msg)
|
||||
self._triggered = True
|
||||
self._result = 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)
|
||||
self._call_listener(listener)
|
||||
|
||||
def add_listener(self, listener):
|
||||
"""
|
||||
Add a callable/awaitable to listen to when this event is triggered.
|
||||
Add a callable/awaitable that listens for the result, when this event
|
||||
is triggered.
|
||||
"""
|
||||
if is_awaitable(listener) or callable(listener):
|
||||
if listener not in self._listeners:
|
||||
self._listeners.append(listener)
|
||||
if self.triggered:
|
||||
# If the event was already triggered, call the listener
|
||||
# immediately with the result.
|
||||
self._call_listener(listener)
|
||||
else:
|
||||
msg = "Listener must be callable or awaitable."
|
||||
raise ValueError(msg)
|
||||
|
||||
def remove_listener(self, *args):
|
||||
"""
|
||||
Clear the specified handler functions in *args. If no handlers
|
||||
provided, clear all handlers.
|
||||
Clear the specified listener functions in *args. If no listeners are
|
||||
provided, clear all the listeners.
|
||||
"""
|
||||
if args:
|
||||
for listener in args:
|
||||
@@ -50,6 +98,15 @@ class Event:
|
||||
else:
|
||||
self._listeners = []
|
||||
|
||||
def _call_listener(self, listener):
|
||||
"""
|
||||
Call the referenced listener with the event's result.
|
||||
"""
|
||||
if is_awaitable(listener):
|
||||
asyncio.create_task(listener(self._result))
|
||||
else:
|
||||
listener(self._result)
|
||||
|
||||
|
||||
def when(target, *args, **kwargs):
|
||||
"""
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -22,6 +22,17 @@ def teardown():
|
||||
container.innerHTML = ""
|
||||
|
||||
|
||||
def test_event_no_result():
|
||||
"""
|
||||
If an event has not been triggered with a result, accessing the result
|
||||
parameter raises a ValueError.
|
||||
"""
|
||||
event = Event()
|
||||
with upytest.raises(ValueError) as e:
|
||||
event.result
|
||||
assert str(e.exception) == "Event has not been triggered yet. No result available."
|
||||
|
||||
|
||||
def test_event_add_listener():
|
||||
"""
|
||||
Adding a listener to an event should add it to the list of listeners. It
|
||||
@@ -35,6 +46,52 @@ def test_event_add_listener():
|
||||
assert listener in event._listeners # The item is the expected listener.
|
||||
|
||||
|
||||
def test_event_add_invalid_listener():
|
||||
"""
|
||||
Adding an invalid listener should raise a ValueError.
|
||||
"""
|
||||
event = Event()
|
||||
with upytest.raises(ValueError) as e:
|
||||
event.add_listener("invalid")
|
||||
assert str(e.exception) == "Listener must be callable or awaitable."
|
||||
|
||||
|
||||
def test_event_add_listener_triggered():
|
||||
"""
|
||||
Adding a listener to an event that has already been triggered should call
|
||||
the listener immediately with the result.
|
||||
"""
|
||||
event = Event()
|
||||
counter = 0
|
||||
|
||||
def listener(x):
|
||||
nonlocal counter
|
||||
counter += 1
|
||||
assert x == "ok"
|
||||
|
||||
event.trigger("ok")
|
||||
event.add_listener(listener)
|
||||
assert counter == 1 # The listener has been triggered with the expected result.
|
||||
|
||||
|
||||
def test_event_add_listener_multiple_times():
|
||||
"""
|
||||
Adding the same listener multiple times should not call it multiple times.
|
||||
"""
|
||||
event = Event()
|
||||
counter = 0
|
||||
|
||||
def listener(x):
|
||||
nonlocal counter
|
||||
counter += 1
|
||||
assert x == "ok"
|
||||
|
||||
event.add_listener(listener)
|
||||
event.add_listener(listener)
|
||||
event.trigger("ok")
|
||||
assert counter == 1 # The listener has been triggered only once.
|
||||
|
||||
|
||||
def test_event_remove_listener():
|
||||
"""
|
||||
Removing a listener from an event should remove it from the list of
|
||||
|
||||
Reference in New Issue
Block a user