mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-19 18:27:29 -05:00
Compare commits
5 Commits
2025.10.1
...
when-updat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58c91b941b | ||
|
|
b33661ff8e | ||
|
|
9db8b13d9c | ||
|
|
3003a9671d | ||
|
|
b87c86f266 |
@@ -76,7 +76,7 @@ Read the [contributing guide](https://docs.pyscript.net/latest/contributing/)
|
||||
to learn about our development process, reporting bugs and improvements,
|
||||
creating issues and asking questions.
|
||||
|
||||
Check out the [developing process](https://docs.pyscript.net/latest/developers/)
|
||||
Check out the [development process](https://docs.pyscript.net/latest/developers/)
|
||||
documentation for more information on how to setup your development environment.
|
||||
|
||||
## Governance
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// ⚠️ This file is an artifact: DO NOT MODIFY
|
||||
export default {
|
||||
"pyscript": {
|
||||
"__init__.py": "from polyscript import lazy_py_modules as py_import\nfrom pyscript.display import HTML,display\nfrom pyscript.fetch import fetch\nfrom pyscript.magic_js import RUNNING_IN_WORKER,PyWorker,config,current_target,document,js_import,js_modules,sync,window\nfrom pyscript.storage import Storage,storage\nfrom pyscript.websocket import WebSocket\nif not RUNNING_IN_WORKER:from pyscript.workers import create_named_worker,workers\ntry:from pyscript.event_handling import when\nexcept:from pyscript.util import NotSupported;when=NotSupported('pyscript.when','pyscript.when currently not available with this interpreter')",
|
||||
"__init__.py": "from polyscript import lazy_py_modules as py_import\nfrom pyscript.event_handling import when\nfrom pyscript.display import HTML,display\nfrom pyscript.fetch import fetch\nfrom pyscript.magic_js import RUNNING_IN_WORKER,PyWorker,config,current_target,document,js_import,js_modules,sync,window\nfrom pyscript.storage import Storage,storage\nfrom pyscript.websocket import WebSocket\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\telif 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\telif 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\telif 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(*D,target=_A,append=True):\n\tC=append;A=target\n\tif A is _A:A=current_target()\n\telif not isinstance(A,str):raise TypeError(f\"target must be str or None, not {A.__class__.__name__}\")\n\telif A=='':raise ValueError('Cannot have an empty target')\n\telif A.startswith('#'):A=A[1:]\n\tB=document.getElementById(A)\n\tif B is _A:raise ValueError(f\"Invalid selector with id={A}. Cannot be found in the page.\")\n\tif B.tagName=='SCRIPT'and hasattr(B,'target'):B=B.target\n\tfor E in D:\n\t\tif not C:B.replaceChildren()\n\t\t_write(B,E,append=C)",
|
||||
"event_handling.py": "import inspect\ntry:from pyodide.ffi.wrappers import add_event_listener\nexcept ImportError:\n\tdef add_event_listener(el,event_type,func):el.addEventListener(event_type,func)\nfrom pyscript.magic_js import document\ndef when(event_type=None,selector=None):\n\tA=selector\n\tdef B(func):\n\t\tB=func;from pyscript.web import Element as E,ElementCollection as F\n\t\tif isinstance(A,str):C=document.querySelectorAll(A)\n\t\telif isinstance(A,E):C=[A._dom_element]\n\t\telif isinstance(A,F):C=[A._dom_element for A in A]\n\t\telif isinstance(A,list):C=A\n\t\telse:C=[A]\n\t\ttry:\n\t\t\tG=inspect.signature(B)\n\t\t\tif not G.parameters:\n\t\t\t\tif inspect.iscoroutinefunction(B):\n\t\t\t\t\tasync def D(*A,**C):await B()\n\t\t\t\telse:\n\t\t\t\t\tdef D(*A,**C):B()\n\t\t\telse:D=B\n\t\texcept AttributeError:\n\t\t\tdef D(*C,**D):\n\t\t\t\ttry:return B(*C,**D)\n\t\t\t\texcept TypeError as A:\n\t\t\t\t\tif'takes'in str(A)and'positional arguments'in str(A):return B()\n\t\t\t\t\traise\n\t\tfor H in C:add_event_listener(H,event_type,D)\n\t\treturn B\n\treturn B",
|
||||
"event_handling.py": "import inspect\ntry:from pyodide.ffi.wrappers import add_event_listener\nexcept ImportError:\n\tdef add_event_listener(el,event_type,func):el.addEventListener(event_type,func)\nfrom pyscript.magic_js import document\ndef when(target,*B,**C):\n\tI='handler';F=target;E=None\n\tif B and callable(B[0]):E=B[0];B=B[1:]\n\telif callable(C.get(I)):E=C.pop(I)\n\tG=hasattr(F,'__when__')\n\tif not G:\n\t\tif B:A=B[0]\n\t\telif C:A=C.get('selector')\n\t\tif not A:raise ValueError('No selector provided.')\n\t\tfrom pyscript.web import Element as J,ElementCollection as K\n\t\tif isinstance(A,str):D=document.querySelectorAll(A)\n\t\telif isinstance(A,J):D=[A._dom_element]\n\t\telif isinstance(A,K):D=[A._dom_element for A in A]\n\t\telif isinstance(A,list):D=A\n\t\telse:D=[A]\n\tdef H(func):\n\t\tA=func\n\t\ttry:\n\t\t\tH=inspect.signature(A)\n\t\t\tif not H.parameters:\n\t\t\t\tif inspect.iscoroutinefunction(A):\n\t\t\t\t\tasync def E(*B,**C):await A()\n\t\t\t\telse:\n\t\t\t\t\tdef E(*B,**C):A()\n\t\t\telse:E=A\n\t\texcept AttributeError:\n\t\t\tdef E(*C,**D):\n\t\t\t\ttry:return A(*C,**D)\n\t\t\t\texcept TypeError as B:\n\t\t\t\t\tif'takes'in str(B)and'positional arguments'in str(B):return A()\n\t\t\t\t\traise\n\t\tif G:F.__when__(E,*B,**C)\n\t\telse:\n\t\t\tfor I in D:add_event_listener(I,F,E)\n\t\treturn A\n\tif E:H(E)\n\telse:return 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):A=value;return isinstance(A,list)or isinstance(A,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 not A in B:B.append(A);A=_loop(_array_keys(A),input,B,A)\n\telif _is_object(A)and not A 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)",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
# as it works transparently in both the main thread and worker cases.
|
||||
|
||||
from polyscript import lazy_py_modules as py_import
|
||||
from pyscript.event_handling import when
|
||||
from pyscript.display import HTML, display
|
||||
from pyscript.fetch import fetch
|
||||
from pyscript.magic_js import (
|
||||
@@ -48,14 +49,3 @@ from pyscript.websocket import WebSocket
|
||||
|
||||
if not RUNNING_IN_WORKER:
|
||||
from pyscript.workers import create_named_worker, workers
|
||||
|
||||
try:
|
||||
from pyscript.event_handling import when
|
||||
except:
|
||||
# TODO: should we remove this? Or at the very least, we should capture
|
||||
# the traceback otherwise it's very hard to debug
|
||||
from pyscript.util import NotSupported
|
||||
|
||||
when = NotSupported(
|
||||
"pyscript.when", "pyscript.when currently not available with this interpreter"
|
||||
)
|
||||
|
||||
@@ -12,14 +12,42 @@ except ImportError:
|
||||
from pyscript.magic_js import document
|
||||
|
||||
|
||||
def when(event_type=None, selector=None):
|
||||
def when(target, *args, **kwargs):
|
||||
"""
|
||||
Decorates a function and passes py-* events to the decorated function
|
||||
The events might or not be an argument of the decorated function
|
||||
A decorator and function for attaching event handlers to DOM elements or
|
||||
whenable objects.
|
||||
|
||||
When used as a decorator, the target is the object that will trigger the
|
||||
event. The handler function is the decorated function. The handler function
|
||||
will be called when the target is triggered.
|
||||
|
||||
When used as a function, the target is the object that will trigger the
|
||||
event. The handler function is the next argument. The remaining arguments
|
||||
and keyword arguments are passed to the target when it is triggered.
|
||||
"""
|
||||
# If "when" is called as a function, try to grab the handler from the
|
||||
# arguments. If there's no handler, this must be a decorator based call.
|
||||
handler = None
|
||||
if args and callable(args[0]):
|
||||
handler = args[0]
|
||||
args = args[1:]
|
||||
elif callable(kwargs.get("handler")):
|
||||
handler = kwargs.pop("handler")
|
||||
|
||||
def decorator(func):
|
||||
|
||||
# Does the target implement the when protocol?
|
||||
whenable = hasattr(target, "__when__")
|
||||
# If not when-able, the DOM selector for the target event.
|
||||
if not whenable:
|
||||
# The target is an event linked to a DOM selector. Extract the
|
||||
# selector from the arguments or keyword arguments.
|
||||
if args:
|
||||
selector = args[0]
|
||||
elif kwargs:
|
||||
selector = kwargs.get("selector")
|
||||
if not selector:
|
||||
# There must be a selector if the target is not when-able.
|
||||
raise ValueError("No selector provided.")
|
||||
# Grab the DOM elements to which the target event will be attached.
|
||||
from pyscript.web import Element, ElementCollection
|
||||
|
||||
if isinstance(selector, str):
|
||||
@@ -36,11 +64,11 @@ def when(event_type=None, selector=None):
|
||||
else:
|
||||
elements = [selector]
|
||||
|
||||
def decorator(func):
|
||||
try:
|
||||
sig = inspect.signature(func)
|
||||
# Function doesn't receive events
|
||||
if not sig.parameters:
|
||||
|
||||
# Function is async: must be awaited
|
||||
if inspect.iscoroutinefunction(func):
|
||||
|
||||
@@ -54,7 +82,6 @@ def when(event_type=None, selector=None):
|
||||
|
||||
else:
|
||||
wrapper = func
|
||||
|
||||
except AttributeError:
|
||||
# TODO: this is very ugly hack to get micropython working because inspect.signature
|
||||
# doesn't exist, but we need to actually properly replace inspect.signature.
|
||||
@@ -68,9 +95,15 @@ def when(event_type=None, selector=None):
|
||||
|
||||
raise
|
||||
|
||||
for el in elements:
|
||||
add_event_listener(el, event_type, wrapper)
|
||||
if whenable:
|
||||
target.__when__(wrapper, *args, **kwargs)
|
||||
else:
|
||||
for el in elements:
|
||||
add_event_listener(el, target, wrapper)
|
||||
|
||||
return func
|
||||
|
||||
return decorator
|
||||
if handler:
|
||||
decorator(handler)
|
||||
else:
|
||||
return decorator
|
||||
|
||||
@@ -214,3 +214,130 @@ def test_when_decorator_invalid_selector():
|
||||
def foo(evt): ...
|
||||
|
||||
assert "'#.bad' is not a valid selector" in str(e.exception), str(e.exception)
|
||||
|
||||
|
||||
def test_when_decorates_a_whenable():
|
||||
"""
|
||||
When the @when decorator is used on a function to handle a whenable object,
|
||||
the function should be called when the whenable object is triggered.
|
||||
"""
|
||||
|
||||
class MyWhenable:
|
||||
"""
|
||||
A simple whenable object that can be triggered.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.handler = None
|
||||
self.args = None
|
||||
self.kwargs = None
|
||||
|
||||
def trigger(self):
|
||||
"""
|
||||
Triggers the whenable object, resulting in the handler being
|
||||
called.
|
||||
"""
|
||||
if self.handler:
|
||||
result = {
|
||||
"args": self.args,
|
||||
"kwargs": self.kwargs,
|
||||
}
|
||||
self.handler(result) # call the handler
|
||||
|
||||
def __when__(self, handler, *args, **kwargs):
|
||||
"""
|
||||
These implementation details depend on the sort of thing the
|
||||
whenable object represents. This is just a simple example.
|
||||
"""
|
||||
self.handler = handler
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
whenable = MyWhenable()
|
||||
counter = 0
|
||||
|
||||
# When as a decorator.
|
||||
@web.when(whenable, "foo", "bar", baz="qux")
|
||||
def foo(result):
|
||||
"""
|
||||
A function that should be called when the whenable object is triggered.
|
||||
|
||||
The result generated by the whenable object should be passed to the
|
||||
function.
|
||||
"""
|
||||
nonlocal counter
|
||||
counter += 1
|
||||
assert result["args"] == ("foo", "bar")
|
||||
assert result["kwargs"] == {"baz": "qux"}
|
||||
|
||||
# The function should not be called until the whenable object is triggered.
|
||||
assert counter == 0
|
||||
# Trigger the whenable object.
|
||||
whenable.trigger()
|
||||
# The function should have been called when the whenable object was
|
||||
# triggered.
|
||||
assert counter == 1
|
||||
|
||||
|
||||
def test_when_called_with_a_whenable():
|
||||
"""
|
||||
The when function should be able to be called with a whenable object,
|
||||
a handler function, and arguments.
|
||||
"""
|
||||
|
||||
class MyWhenable:
|
||||
"""
|
||||
A simple whenable object that can be triggered.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.handler = None
|
||||
self.args = None
|
||||
self.kwargs = None
|
||||
|
||||
def trigger(self):
|
||||
"""
|
||||
Triggers the whenable object, resulting in the handler being
|
||||
called.
|
||||
"""
|
||||
if self.handler:
|
||||
result = {
|
||||
"args": self.args,
|
||||
"kwargs": self.kwargs,
|
||||
}
|
||||
self.handler(result) # call the handler
|
||||
|
||||
def __when__(self, handler, *args, **kwargs):
|
||||
"""
|
||||
These implementation details depend on the sort of thing the
|
||||
whenable object represents. This is just a simple example.
|
||||
"""
|
||||
self.handler = handler
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
whenable = MyWhenable()
|
||||
counter = 0
|
||||
|
||||
def handler(result):
|
||||
"""
|
||||
A function that should be called when the whenable object is triggered.
|
||||
|
||||
The result generated by the whenable object should be passed to the
|
||||
function.
|
||||
"""
|
||||
nonlocal counter
|
||||
counter += 1
|
||||
assert result["args"] == ("foo", "bar")
|
||||
assert result["kwargs"] == {"baz": "qux"}
|
||||
|
||||
# When as a function.
|
||||
web.when(whenable, handler, "foo", "bar", baz="qux")
|
||||
|
||||
# The function should not be called until the whenable object is triggered.
|
||||
assert counter == 0
|
||||
# Trigger the whenable object.
|
||||
whenable.trigger()
|
||||
# The function should have been called when the whenable object was
|
||||
# triggered.
|
||||
assert counter == 1
|
||||
|
||||
Reference in New Issue
Block a user