From d6f401a0c49776bf0a4dc9d2525e6144760f0e3c Mon Sep 17 00:00:00 2001 From: Nicholas Tollervey Date: Thu, 5 Feb 2026 12:31:53 +0000 Subject: [PATCH] Refine CSS classes remove method to only warn if passed a non-existent path (previously would throw an exception). (#2450) * Refine CSS classes remove method to only warn if passed a non-existent path (previously would throw an exception). * Refine console import. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- core/src/stdlib/pyscript.js | 2 +- core/src/stdlib/pyscript/web.py | 37 ++++++++++++++++++++++------- core/tests/python/tests/test_web.py | 4 ++++ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/core/src/stdlib/pyscript.js b/core/src/stdlib/pyscript.js index acaa8cd7..8b288bd7 100644 --- a/core/src/stdlib/pyscript.js +++ b/core/src/stdlib/pyscript.js @@ -12,7 +12,7 @@ export default { "media.py": "from pyscript import window\nfrom pyscript.ffi import to_js\nclass Device:\n\tdef __init__(A,device):A._device_info=device\n\t@property\n\tdef id(self):return self._device_info.deviceId\n\t@property\n\tdef group(self):return self._device_info.groupId\n\t@property\n\tdef kind(self):return self._device_info.kind\n\t@property\n\tdef label(self):return self._device_info.label\n\tdef __getitem__(A,key):return getattr(A,key)\n\t@classmethod\n\tasync def request_stream(F,audio=False,video=True):\n\t\tE='video';D='audio';C=video;B=audio;A={}\n\t\tif isinstance(B,bool):A[D]=B\n\t\telif isinstance(B,dict):A[D]=B\n\t\tif isinstance(C,bool):A[E]=C\n\t\telif isinstance(C,dict):A[E]=C\n\t\treturn await window.navigator.mediaDevices.getUserMedia(to_js(A))\n\t@classmethod\n\tasync def load(A,audio=False,video=True):return await A.request_stream(audio=audio,video=video)\n\tasync def get_stream(A):B=A.kind.replace('input','').replace('output','');C={B:{'deviceId':{'exact':A.id}}};return await A.request_stream(**C)\nasync def list_devices():A=await window.navigator.mediaDevices.enumerateDevices();return[Device(A)for A in A]", "storage.py": "_C='memoryview'\n_B='bytearray'\n_A='generic'\nfrom polyscript import storage as _polyscript_storage\nfrom pyscript.flatted import parse as _parse\nfrom pyscript.flatted import stringify as _stringify\nfrom pyscript.ffi import is_none\ndef _convert_to_idb(value):\n\tA=value\n\tif is_none(A):return _stringify(['null',0])\n\tif isinstance(A,(bool,float,int,str,list,dict,tuple)):return _stringify([_A,A])\n\tif isinstance(A,bytearray):return _stringify([_B,list(A)])\n\tif isinstance(A,memoryview):return _stringify([_C,list(A)])\n\traise TypeError(f\"Cannot serialize type {type(A).__name__} for storage.\")\ndef _convert_from_idb(value):\n\tC=value;A,B=_parse(C)\n\tif A=='null':return\n\tif A==_A:return B\n\tif A==_B:return bytearray(B)\n\tif A==_C:return memoryview(bytearray(B))\n\treturn C\nclass Storage(dict):\n\tdef __init__(B,store):A=store;super().__init__({A:_convert_from_idb(B)for(A,B)in A.entries()});B._store=A\n\tdef __delitem__(A,key):A._store.delete(key);super().__delitem__(key)\n\tdef __setitem__(B,key,value):A=value;B._store.set(key,_convert_to_idb(A));super().__setitem__(key,A)\n\tdef clear(A):A._store.clear();super().clear()\n\tasync def sync(A):await A._store.sync()\nasync def storage(name='',storage_class=Storage):\n\tif not name:raise ValueError('Storage name must be a non-empty string')\n\tA=await _polyscript_storage(f\"@pyscript/{name}\");return storage_class(A)", "util.py": "import js,inspect\ndef as_bytearray(buffer):\n\tA=js.Uint8Array.new(buffer);B=A.length;C=bytearray(B)\n\tfor D in range(B):C[D]=A[D]\n\treturn C\nclass NotSupported:\n\tdef __init__(A,name,error):object.__setattr__(A,'name',name);object.__setattr__(A,'error',error)\n\tdef __repr__(A):return f\"\"\n\tdef __getattr__(A,attr):raise AttributeError(A.error)\n\tdef __setattr__(A,attr,value):raise AttributeError(A.error)\n\tdef __call__(A,*B):raise TypeError(A.error)\ndef is_awaitable(obj):\n\tA=obj;from pyscript import config as C\n\tif C['type']=='mpy':\n\t\tB=repr(A)\n\t\tif''in B:return True\n\t\tif''in B:return True\n\t\treturn inspect.isgeneratorfunction(A)\n\treturn inspect.iscoroutinefunction(A)", - "web.py": "_D='object'\n_C='tagName'\n_B='on_'\n_A=None\nfrom pyscript import document,when,Event\nfrom pyscript.ffi import create_proxy,is_none\ndef _wrap_if_not_none(dom_element):return Element.wrap_dom_element(dom_element)if not is_none(dom_element)else _A\ndef _find_by_id(dom_node,target_id):element_id=target_id[1:]if target_id.startswith('#')else target_id;result=dom_node.querySelector(f\"#{element_id}\");return _wrap_if_not_none(result)\ndef _find_and_wrap(dom_node,selector):return ElementCollection.wrap_dom_elements(dom_node.querySelectorAll(selector))\nclass Element:\n\telement_classes_by_tag_name={}\n\t@classmethod\n\tdef get_tag_name(cls):return cls.__name__.replace('_','')\n\t@classmethod\n\tdef register_element_classes(cls,element_classes):\n\t\tfor element_class in element_classes:tag_name=element_class.get_tag_name();cls.element_classes_by_tag_name[tag_name]=element_class\n\t@classmethod\n\tdef unregister_element_classes(cls,element_classes):\n\t\tfor element_class in element_classes:tag_name=element_class.get_tag_name();cls.element_classes_by_tag_name.pop(tag_name,_A)\n\t@classmethod\n\tdef wrap_dom_element(cls,dom_element):element_cls=cls.element_classes_by_tag_name.get(dom_element.tagName.lower(),cls);return element_cls(dom_element=dom_element)\n\tdef __init__(self,dom_element=_A,classes=_A,style=_A,**kwargs):\n\t\tif is_none(dom_element):self._dom_element=document.createElement(type(self).get_tag_name())\n\t\telse:self._dom_element=dom_element\n\t\tself._on_events={};self.update(classes=classes,style=style,**kwargs)\n\tdef __eq__(self,obj):return isinstance(obj,Element)and obj._dom_element==self._dom_element\n\tdef __getitem__(self,key):\n\t\tif isinstance(key,(int,slice)):return self.children[key]\n\t\tif isinstance(key,str):return _find_by_id(self._dom_element,key)\n\t\traise TypeError(f\"Element indices must be integers, slices, or strings, not {type(key).__name__}.\")\n\tdef __getattr__(self,name):\n\t\tif name.startswith(_B):return self.get_event(name)\n\t\tdom_name=self._normalize_attribute_name(name);return getattr(self._dom_element,dom_name)\n\tdef __setattr__(self,name,value):\n\t\tif name.startswith('_'):super().__setattr__(name,value)\n\t\telif name.startswith(_B):self.get_event(name).add_listener(value)\n\t\telse:dom_name=self._normalize_attribute_name(name);setattr(self._dom_element,dom_name,value)\n\tdef _normalize_attribute_name(self,name):\n\t\tif name.endswith('_'):name=name[:-1]\n\t\tif name=='for':return'htmlFor'\n\t\tif name=='class':return'className'\n\t\treturn name\n\tdef get_event(self,name):\n\t\tif not name.startswith(_B):raise ValueError(\"Event names must start with 'on_'.\")\n\t\tevent_name=name[3:]\n\t\tif not hasattr(self._dom_element,event_name):raise ValueError(f\"Element has no '{event_name}' event.\")\n\t\tif name in self._on_events:return self._on_events[name]\n\t\tev=Event();self._on_events[name]=ev;self._dom_element.addEventListener(event_name,create_proxy(ev.trigger));return ev\n\t@property\n\tdef children(self):return ElementCollection.wrap_dom_elements(self._dom_element.children)\n\t@property\n\tdef classes(self):\n\t\tif not hasattr(self,'_classes'):self._classes=Classes(self)\n\t\treturn self._classes\n\t@property\n\tdef style(self):\n\t\tif not hasattr(self,'_style'):self._style=Style(self)\n\t\treturn self._style\n\t@property\n\tdef parent(self):\n\t\tif is_none(self._dom_element.parentElement):return\n\t\treturn Element.wrap_dom_element(self._dom_element.parentElement)\n\tdef append(self,*items):\n\t\tfor item in items:\n\t\t\tif isinstance(item,Element):self._dom_element.appendChild(item._dom_element)\n\t\t\telif isinstance(item,ElementCollection):\n\t\t\t\tfor element in item:self._dom_element.appendChild(element._dom_element)\n\t\t\telif isinstance(item,(list,tuple)):\n\t\t\t\tfor child in item:self.append(child)\n\t\t\telif hasattr(item,_C):self._dom_element.appendChild(item)\n\t\t\telif hasattr(item,'length'):\n\t\t\t\tfor element in item:self._dom_element.appendChild(element)\n\t\t\telse:raise TypeError(f\"Cannot append {type(item).__name__} to element.\")\n\tdef clone(self,clone_id=_A):clone=Element.wrap_dom_element(self._dom_element.cloneNode(True));clone.id=clone_id;return clone\n\tdef find(self,selector):return _find_and_wrap(self._dom_element,selector)\n\tdef show_me(self):self._dom_element.scrollIntoView()\n\tdef update(self,classes=_A,style=_A,**kwargs):\n\t\tif classes:\n\t\t\tif isinstance(classes,str):self.classes.add(classes)\n\t\t\telse:\n\t\t\t\tfor class_name in classes:self.classes.add(class_name)\n\t\tif style:\n\t\t\tfor(key,value)in style.items():self.style[key]=value\n\t\tfor(name,value)in kwargs.items():setattr(self,name,value)\nclass Classes(set):\n\tdef __init__(self,element):self._class_list=element._dom_element.classList;super().__init__(self._class_list)\n\tdef _extract_class_names(self,class_name):return[name for name in class_name.split()if name]if' 'in class_name else[class_name]\n\tdef add(self,class_name):\n\t\tfor name in self._extract_class_names(class_name):super().add(name);self._class_list.add(name)\n\tdef remove(self,class_name):\n\t\tfor name in self._extract_class_names(class_name):super().remove(name);self._class_list.remove(name)\n\tdef discard(self,class_name):\n\t\tfor name in self._extract_class_names(class_name):\n\t\t\tsuper().discard(name)\n\t\t\tif name in self._class_list:self._class_list.remove(name)\n\tdef clear(self):\n\t\tsuper().clear()\n\t\twhile self._class_list.length>0:self._class_list.remove(self._class_list.item(0))\nclass Style(dict):\n\tdef __init__(self,element):self._style=element._dom_element.style;super().__init__()\n\tdef __setitem__(self,key,value):super().__setitem__(key,value);self._style.setProperty(key,str(value))\n\tdef __delitem__(self,key):super().__delitem__(key);self._style.removeProperty(key)\nclass HasOptions:\n\t@property\n\tdef options(self):\n\t\tif not hasattr(self,'_options'):self._options=Options(self)\n\t\treturn self._options\nclass Options:\n\tdef __init__(self,element):self._element=element\n\tdef __getitem__(self,key):return self.options[key]\n\tdef __iter__(self):yield from self.options\n\tdef __len__(self):return len(self.options)\n\tdef __repr__(self):return f\"{self.__class__.__name__} (length: {len(self)}) {self.options}\"\n\t@property\n\tdef options(self):return[Element.wrap_dom_element(o)for o in self._element._dom_element.options]\n\t@property\n\tdef selected(self):return self.options[self._element._dom_element.selectedIndex]\n\tdef add(self,value=_A,html=_A,text=_A,before=_A,**kwargs):\n\t\tif value:kwargs['value']=value\n\t\tif html:kwargs['innerHTML']=html\n\t\tif text:kwargs['text']=text\n\t\tnew_option=option(**kwargs)\n\t\tif before and isinstance(before,Element):before=before._dom_element\n\t\tself._element._dom_element.add(new_option._dom_element,before)\n\tdef clear(self):self._element._dom_element.length=0\n\tdef remove(self,index):self._element._dom_element.remove(index)\nclass ContainerElement(Element):\n\tdef __init__(self,*args,children=_A,dom_element=_A,style=_A,classes=_A,**kwargs):\n\t\tsuper().__init__(dom_element=dom_element,style=style,classes=classes,**kwargs)\n\t\tfor child in list(args)+(children or[]):\n\t\t\tif isinstance(child,(Element,ElementCollection)):self.append(child)\n\t\t\telse:self._dom_element.insertAdjacentHTML('beforeend',child)\n\tdef __iter__(self):yield from self.children\nclass ElementCollection:\n\t@classmethod\n\tdef wrap_dom_elements(cls,dom_elements):return cls([Element.wrap_dom_element(dom_element)for dom_element in dom_elements])\n\tdef __init__(self,elements):self._elements=elements\n\tdef __eq__(self,obj):return isinstance(obj,ElementCollection)and obj._elements==self._elements\n\tdef __getitem__(self,key):\n\t\tif isinstance(key,int):return self._elements[key]\n\t\tif isinstance(key,slice):return ElementCollection(self._elements[key])\n\t\tif isinstance(key,str):\n\t\t\tfor element in self._elements:\n\t\t\t\tresult=_find_by_id(element._dom_element,key)\n\t\t\t\tif result:return result\n\t\t\treturn\n\t\traise TypeError(f\"Collection indices must be integers, slices, or strings, not {type(key).__name__}\")\n\tdef __iter__(self):yield from self._elements\n\tdef __len__(self):return len(self._elements)\n\tdef __repr__(self):return f\"{self.__class__.__name__} (length: {len(self._elements)}) {self._elements}\"\n\t@property\n\tdef elements(self):return self._elements\n\tdef find(self,selector):\n\t\telements=[]\n\t\tfor element in self._elements:elements.extend(_find_and_wrap(element._dom_element,selector))\n\t\treturn ElementCollection(elements)\n\tdef update_all(self,**kwargs):\n\t\tfor element in self._elements:\n\t\t\tfor(name,value)in kwargs.items():setattr(element,name,value)\nclass canvas(ContainerElement):\n\tdef download(self,filename='snapped.png'):download_link=a(download=filename,href=self._dom_element.toDataURL());self.append(download_link);download_link._dom_element.click()\n\tdef draw(self,what,width=_A,height=_A):\n\t\tif isinstance(what,Element):what=what._dom_element\n\t\tctx=self._dom_element.getContext('2d')\n\t\tif width or height:ctx.drawImage(what,0,0,width,height)\n\t\telse:ctx.drawImage(what,0,0)\nclass video(ContainerElement):\n\tdef snap(self,to=_A,width=_A,height=_A):\n\t\tB='CANVAS';A='Element to snap to must be a canvas.';width=width if width else self.videoWidth;height=height if height else self.videoHeight\n\t\tif is_none(to):to=canvas(width=width,height=height)\n\t\telif isinstance(to,Element):\n\t\t\tif to.tag!='canvas':raise TypeError(A)\n\t\telif getattr(to,_C,'')==B:to=canvas(dom_element=to)\n\t\telif isinstance(to,str):\n\t\t\tnodelist=document.querySelectorAll(to)\n\t\t\tif nodelist.length==0:raise TypeError(f\"No element with selector {to} to snap to.\")\n\t\t\tif nodelist[0].tagName!=B:raise TypeError(A)\n\t\t\tto=canvas(dom_element=nodelist[0])\n\t\tto.draw(self,width,height);return to\nclass datalist(ContainerElement,HasOptions):0\nclass optgroup(ContainerElement,HasOptions):0\nclass select(ContainerElement,HasOptions):0\nCONTAINER_TAGS=['a','abbr','address','article','aside','audio','b','blockquote','body','button','caption','cite','code','colgroup','data','dd','del','details','dialog','div','dl','dt','em','fieldset','figcaption','figure','footer','form','h1','h2','h3','h4','h5','h6','head','header','hgroup','html','i','iframe','ins','kbd','label','legend','li','main','map','mark','menu','meta','meter','nav',_D,'ol','option','output','p','param','picture','pre','progress','q','s','script','section','small','span','strong','style','sub','summary','sup','table','tbody','td','template','textarea','tfoot','th','thead','time','title','tr','u','ul','var','wbr']\nVOID_TAGS=['area','base','br','col','embed','hr','img','input','link','source','track']\ndef _create_element_classes():\n\tA='__doc__';classes=[canvas,video,datalist,optgroup,select]\n\tfor tag in CONTAINER_TAGS:class_name=f\"{tag}_\"if tag in('del','map',_D)else tag;doc=f\"HTML <{tag}> element. Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/{tag}\";cls=type(class_name,(ContainerElement,),{A:doc});globals()[class_name]=cls;classes.append(cls)\n\tfor tag in VOID_TAGS:class_name=f\"{tag}_\"if tag=='input'else tag;doc=f\"HTML <{tag}> element. Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/{tag}\";cls=type(class_name,(Element,),{A:doc});globals()[class_name]=cls;classes.append(cls)\n\tElement.register_element_classes(classes)\n_create_element_classes()\nclass Page:\n\tdef __init__(self):self.html=Element.wrap_dom_element(document.documentElement);self.body=Element.wrap_dom_element(document.body);self.head=Element.wrap_dom_element(document.head)\n\tdef __getitem__(self,key):return _find_by_id(document,key)\n\t@property\n\tdef title(self):return document.title\n\t@title.setter\n\tdef title(self,value):document.title=value\n\tdef append(self,*items):self.body.append(*items)\n\tdef find(self,selector):return _find_and_wrap(document,selector)\npage=Page()", + "web.py": "_D='object'\n_C='tagName'\n_B='on_'\n_A=None\nfrom js import console\nfrom pyscript import document,Event\nfrom pyscript.ffi import create_proxy,is_none\ndef _wrap_if_not_none(dom_element):return Element.wrap_dom_element(dom_element)if not is_none(dom_element)else _A\ndef _find_by_id(dom_node,target_id):element_id=target_id[1:]if target_id.startswith('#')else target_id;result=dom_node.querySelector(f\"#{element_id}\");return _wrap_if_not_none(result)\ndef _find_and_wrap(dom_node,selector):return ElementCollection.wrap_dom_elements(dom_node.querySelectorAll(selector))\nclass Element:\n\telement_classes_by_tag_name={}\n\t@classmethod\n\tdef get_tag_name(cls):return cls.__name__.replace('_','')\n\t@classmethod\n\tdef register_element_classes(cls,element_classes):\n\t\tfor element_class in element_classes:tag_name=element_class.get_tag_name();cls.element_classes_by_tag_name[tag_name]=element_class\n\t@classmethod\n\tdef unregister_element_classes(cls,element_classes):\n\t\tfor element_class in element_classes:tag_name=element_class.get_tag_name();cls.element_classes_by_tag_name.pop(tag_name,_A)\n\t@classmethod\n\tdef wrap_dom_element(cls,dom_element):element_cls=cls.element_classes_by_tag_name.get(dom_element.tagName.lower(),cls);return element_cls(dom_element=dom_element)\n\tdef __init__(self,dom_element=_A,classes=_A,style=_A,**kwargs):\n\t\tif is_none(dom_element):self._dom_element=document.createElement(type(self).get_tag_name())\n\t\telse:self._dom_element=dom_element\n\t\tself._on_events={};self.update(classes=classes,style=style,**kwargs)\n\tdef __eq__(self,obj):return isinstance(obj,Element)and obj._dom_element==self._dom_element\n\tdef __getitem__(self,key):\n\t\tif isinstance(key,(int,slice)):return self.children[key]\n\t\tif isinstance(key,str):return _find_by_id(self._dom_element,key)\n\t\traise TypeError(f\"Element indices must be integers, slices, or strings, not {type(key).__name__}.\")\n\tdef __getattr__(self,name):\n\t\tif name.startswith(_B):return self.get_event(name)\n\t\tdom_name=self._normalize_attribute_name(name);return getattr(self._dom_element,dom_name)\n\tdef __setattr__(self,name,value):\n\t\tif name.startswith('_'):super().__setattr__(name,value)\n\t\telif name.startswith(_B):self.get_event(name).add_listener(value)\n\t\telse:dom_name=self._normalize_attribute_name(name);setattr(self._dom_element,dom_name,value)\n\tdef _normalize_attribute_name(self,name):\n\t\tif name.endswith('_'):name=name[:-1]\n\t\tif name=='for':return'htmlFor'\n\t\tif name=='class':return'className'\n\t\treturn name\n\tdef get_event(self,name):\n\t\tif not name.startswith(_B):raise ValueError(\"Event names must start with 'on_'.\")\n\t\tevent_name=name[3:]\n\t\tif not hasattr(self._dom_element,event_name):raise ValueError(f\"Element has no '{event_name}' event.\")\n\t\tif name in self._on_events:return self._on_events[name]\n\t\tev=Event();self._on_events[name]=ev;self._dom_element.addEventListener(event_name,create_proxy(ev.trigger));return ev\n\t@property\n\tdef children(self):return ElementCollection.wrap_dom_elements(self._dom_element.children)\n\t@property\n\tdef classes(self):\n\t\tif not hasattr(self,'_classes'):self._classes=Classes(self)\n\t\treturn self._classes\n\t@property\n\tdef style(self):\n\t\tif not hasattr(self,'_style'):self._style=Style(self)\n\t\treturn self._style\n\t@property\n\tdef parent(self):\n\t\tif is_none(self._dom_element.parentElement):return\n\t\treturn Element.wrap_dom_element(self._dom_element.parentElement)\n\tdef append(self,*items):\n\t\tfor item in items:\n\t\t\tif isinstance(item,Element):self._dom_element.appendChild(item._dom_element)\n\t\t\telif isinstance(item,ElementCollection):\n\t\t\t\tfor element in item:self._dom_element.appendChild(element._dom_element)\n\t\t\telif isinstance(item,(list,tuple)):\n\t\t\t\tfor child in item:self.append(child)\n\t\t\telif hasattr(item,_C):self._dom_element.appendChild(item)\n\t\t\telif hasattr(item,'length'):\n\t\t\t\tfor element in item:self._dom_element.appendChild(element)\n\t\t\telif isinstance(item,(str,int,float,bool)):self._dom_element.append(item)\n\t\t\telse:raise TypeError(f\"Cannot append {type(item).__name__} to element.\")\n\tdef clone(self,clone_id=_A):clone=Element.wrap_dom_element(self._dom_element.cloneNode(True));clone.id=clone_id;return clone\n\tdef find(self,selector):return _find_and_wrap(self._dom_element,selector)\n\tdef show_me(self):self._dom_element.scrollIntoView()\n\tdef update(self,classes=_A,style=_A,**kwargs):\n\t\tif classes:\n\t\t\tif isinstance(classes,str):self.classes.add(classes)\n\t\t\telse:\n\t\t\t\tfor class_name in classes:self.classes.add(class_name)\n\t\tif style:\n\t\t\tfor(key,value)in style.items():self.style[key]=value\n\t\tfor(name,value)in kwargs.items():setattr(self,name,value)\nclass Classes(set):\n\tdef __init__(self,element):self._class_list=element._dom_element.classList;super().__init__(self._class_list)\n\tdef _extract_class_names(self,class_name):return[name for name in class_name.split()if name]if' 'in class_name else[class_name]\n\tdef add(self,class_name):\n\t\tfor name in self._extract_class_names(class_name):super().add(name);self._class_list.add(name)\n\tdef remove(self,class_name):\n\t\tfor name in self._extract_class_names(class_name):\n\t\t\tif name in self:super().remove(name);self._class_list.remove(name)\n\t\t\telse:console.warn(f\"Class '{name}' not found in element classes.\")\n\tdef discard(self,class_name):\n\t\tfor name in self._extract_class_names(class_name):\n\t\t\tsuper().discard(name)\n\t\t\tif name in self._class_list:self._class_list.remove(name)\n\tdef clear(self):\n\t\tsuper().clear()\n\t\twhile self._class_list.length>0:self._class_list.remove(self._class_list.item(0))\nclass Style(dict):\n\tdef __init__(self,element):self._style=element._dom_element.style;super().__init__()\n\tdef __setitem__(self,key,value):super().__setitem__(key,value);self._style.setProperty(key,str(value))\n\tdef __delitem__(self,key):super().__delitem__(key);self._style.removeProperty(key)\nclass HasOptions:\n\t@property\n\tdef options(self):\n\t\tif not hasattr(self,'_options'):self._options=Options(self)\n\t\treturn self._options\nclass Options:\n\tdef __init__(self,element):self._element=element\n\tdef __getitem__(self,key):return self.options[key]\n\tdef __iter__(self):yield from self.options\n\tdef __len__(self):return len(self.options)\n\tdef __repr__(self):return f\"{self.__class__.__name__} (length: {len(self)}) {self.options}\"\n\t@property\n\tdef options(self):return[Element.wrap_dom_element(o)for o in self._element._dom_element.options]\n\t@property\n\tdef selected(self):return self.options[self._element._dom_element.selectedIndex]\n\tdef add(self,value=_A,html=_A,text=_A,before=_A,**kwargs):\n\t\tif value:kwargs['value']=value\n\t\tif html:kwargs['innerHTML']=html\n\t\tif text:kwargs['text']=text\n\t\tnew_option=option(**kwargs)\n\t\tif before and isinstance(before,Element):before=before._dom_element\n\t\tself._element._dom_element.add(new_option._dom_element,before)\n\tdef clear(self):self._element._dom_element.length=0\n\tdef remove(self,index):self._element._dom_element.remove(index)\nclass ContainerElement(Element):\n\tdef __init__(self,*args,children=_A,dom_element=_A,style=_A,classes=_A,**kwargs):\n\t\tsuper().__init__(dom_element=dom_element,style=style,classes=classes,**kwargs)\n\t\tfor child in list(args)+(children or[]):\n\t\t\tif isinstance(child,(Element,ElementCollection)):self.append(child)\n\t\t\telse:self._dom_element.insertAdjacentHTML('beforeend',child)\n\tdef __iter__(self):yield from self.children\nclass ElementCollection:\n\t@classmethod\n\tdef wrap_dom_elements(cls,dom_elements):return cls([Element.wrap_dom_element(dom_element)for dom_element in dom_elements])\n\tdef __init__(self,elements):self._elements=elements\n\tdef __eq__(self,obj):return isinstance(obj,ElementCollection)and obj._elements==self._elements\n\tdef __getitem__(self,key):\n\t\tif isinstance(key,int):return self._elements[key]\n\t\tif isinstance(key,slice):return ElementCollection(self._elements[key])\n\t\tif isinstance(key,str):\n\t\t\tfor element in self._elements:\n\t\t\t\tresult=_find_by_id(element._dom_element,key)\n\t\t\t\tif result:return result\n\t\t\treturn\n\t\traise TypeError(f\"Collection indices must be integers, slices, or strings, not {type(key).__name__}\")\n\tdef __iter__(self):yield from self._elements\n\tdef __len__(self):return len(self._elements)\n\tdef __repr__(self):return f\"{self.__class__.__name__} (length: {len(self._elements)}) {self._elements}\"\n\t@property\n\tdef elements(self):return self._elements\n\tdef find(self,selector):\n\t\telements=[]\n\t\tfor element in self._elements:elements.extend(_find_and_wrap(element._dom_element,selector))\n\t\treturn ElementCollection(elements)\n\tdef update_all(self,**kwargs):\n\t\tfor element in self._elements:\n\t\t\tfor(name,value)in kwargs.items():setattr(element,name,value)\nclass canvas(ContainerElement):\n\tdef download(self,filename='snapped.png'):download_link=a(download=filename,href=self._dom_element.toDataURL());self.append(download_link);download_link._dom_element.click()\n\tdef draw(self,what,width=_A,height=_A):\n\t\tif isinstance(what,Element):what=what._dom_element\n\t\tctx=self._dom_element.getContext('2d')\n\t\tif width or height:ctx.drawImage(what,0,0,width,height)\n\t\telse:ctx.drawImage(what,0,0)\nclass video(ContainerElement):\n\tdef snap(self,to=_A,width=_A,height=_A):\n\t\tB='CANVAS';A='Element to snap to must be a canvas.';width=width if width else self.videoWidth;height=height if height else self.videoHeight\n\t\tif is_none(to):to=canvas(width=width,height=height)\n\t\telif isinstance(to,Element):\n\t\t\tif to.tag!='canvas':raise TypeError(A)\n\t\telif getattr(to,_C,'')==B:to=canvas(dom_element=to)\n\t\telif isinstance(to,str):\n\t\t\tnodelist=document.querySelectorAll(to)\n\t\t\tif nodelist.length==0:raise TypeError(f\"No element with selector {to} to snap to.\")\n\t\t\tif nodelist[0].tagName!=B:raise TypeError(A)\n\t\t\tto=canvas(dom_element=nodelist[0])\n\t\tto.draw(self,width,height);return to\nclass datalist(ContainerElement,HasOptions):0\nclass optgroup(ContainerElement,HasOptions):0\nclass select(ContainerElement,HasOptions):0\nCONTAINER_TAGS=['a','abbr','address','article','aside','audio','b','blockquote','body','button','caption','cite','code','colgroup','data','dd','del','details','dialog','div','dl','dt','em','fieldset','figcaption','figure','footer','form','h1','h2','h3','h4','h5','h6','head','header','hgroup','html','i','iframe','ins','kbd','label','legend','li','main','map','mark','menu','meta','meter','nav',_D,'ol','option','output','p','param','picture','pre','progress','q','s','script','section','small','span','strong','style','sub','summary','sup','table','tbody','td','template','textarea','tfoot','th','thead','time','title','tr','u','ul','var','wbr']\nVOID_TAGS=['area','base','br','col','embed','hr','img','input','link','source','track']\ndef _create_element_classes():\n\tA='__doc__';classes=[canvas,video,datalist,optgroup,select]\n\tfor tag in CONTAINER_TAGS:class_name=f\"{tag}_\"if tag in('del','map',_D)else tag;doc=f\"HTML <{tag}> element. Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/{tag}\";cls=type(class_name,(ContainerElement,),{A:doc});globals()[class_name]=cls;classes.append(cls)\n\tfor tag in VOID_TAGS:class_name=f\"{tag}_\"if tag=='input'else tag;doc=f\"HTML <{tag}> element. Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/{tag}\";cls=type(class_name,(Element,),{A:doc});globals()[class_name]=cls;classes.append(cls)\n\tElement.register_element_classes(classes)\n_create_element_classes()\nclass Page:\n\tdef __init__(self):self.html=Element.wrap_dom_element(document.documentElement);self.body=Element.wrap_dom_element(document.body);self.head=Element.wrap_dom_element(document.head)\n\tdef __getitem__(self,key):return _find_by_id(document,key)\n\t@property\n\tdef title(self):return document.title\n\t@title.setter\n\tdef title(self,value):document.title=value\n\tdef append(self,*items):self.body.append(*items)\n\tdef find(self,selector):return _find_and_wrap(document,selector)\npage=Page()", "websocket.py": "import js\nfrom pyscript.ffi import create_proxy\nfrom pyscript.util import as_bytearray,is_awaitable\ndef _attach_event_handler(websocket,handler_name,handler_function):\n\tA=handler_function\n\tif is_awaitable(A):\n\t\tasync def C(event):await A(WebSocketEvent(event))\n\t\tB=create_proxy(C)\n\telse:B=create_proxy(lambda event:A(WebSocketEvent(event)))\n\tsetattr(websocket,handler_name,B)\nclass WebSocketEvent:\n\tdef __init__(A,event):A._event=event\n\tdef __getattr__(B,attr):\n\t\tA=getattr(B._event,attr)\n\t\tif attr=='data'and not isinstance(A,str):\n\t\t\tif hasattr(A,'to_py'):return A.to_py()\n\t\t\telse:return memoryview(as_bytearray(A))\n\t\treturn A\nclass WebSocket:\n\tCONNECTING=0;OPEN=1;CLOSING=2;CLOSED=3\n\tdef __init__(B,url,protocols=None,**D):\n\t\tC=protocols\n\t\tif C:A=js.WebSocket.new(url,C)\n\t\telse:A=js.WebSocket.new(url)\n\t\tA.binaryType='arraybuffer';object.__setattr__(B,'_js_websocket',A)\n\t\tfor(E,F)in D.items():setattr(B,E,F)\n\tdef __getattr__(A,attr):return getattr(A._js_websocket,attr)\n\tdef __setattr__(B,attr,value):\n\t\tC=value;A=attr\n\t\tif A in['onclose','onerror','onmessage','onopen']:_attach_event_handler(B._js_websocket,A,C)\n\t\telse:setattr(B._js_websocket,A,C)\n\tdef send(B,data):\n\t\tA=data\n\t\tif isinstance(A,str):B._js_websocket.send(A)\n\t\telse:\n\t\t\tC=js.Uint8Array.new(len(A))\n\t\t\tfor(D,E)in enumerate(A):C[D]=E\n\t\t\tB._js_websocket.send(C)\n\tdef close(B,code=None,reason=None):\n\t\tC=reason;A=code\n\t\tif A and C:B._js_websocket.close(A,C)\n\t\telif A:B._js_websocket.close(A)\n\t\telse:B._js_websocket.close()", "workers.py": "import js,json\nfrom polyscript import workers as _polyscript_workers\nclass _ReadOnlyWorkersProxy:\n\tdef __getitem__(A,name):return js.Reflect.get(_polyscript_workers,name)\n\tdef __getattr__(A,name):return js.Reflect.get(_polyscript_workers,name)\nworkers=_ReadOnlyWorkersProxy()\nasync def create_named_worker(src,name,config=None,type='py'):\n\tB=config;A=js.document.createElement('script');A.type=type;A.src=src;A.setAttribute('worker','');A.setAttribute('name',name)\n\tif B:\n\t\tif isinstance(B,str):C=B\n\t\telse:C=json.dumps(B)\n\t\tA.setAttribute('config',C)\n\tjs.document.body.append(A);return await workers[name]" } diff --git a/core/src/stdlib/pyscript/web.py b/core/src/stdlib/pyscript/web.py index b4c2fde1..7b2f98c3 100644 --- a/core/src/stdlib/pyscript/web.py +++ b/core/src/stdlib/pyscript/web.py @@ -201,7 +201,8 @@ document and provides access to common elements like `page.body` and methods like `page.find()` for querying the DOM. """ -from pyscript import document, when, Event # noqa: F401 +from js import console +from pyscript import document, Event # noqa: F401 from pyscript.ffi import create_proxy, is_none @@ -719,13 +720,15 @@ class Element: class Classes(set): """ + Manages CSS classes for an element. + Behaves like a Python `set` with changes automatically reflected in the element's `classList`. ```python # Add and remove classes. element.classes.add("active") - element.classes.remove("inactive") + element.classes.remove("inactive") # Warns if not present. element.classes.discard("maybe-missing") # No error if absent. # Check membership. @@ -742,7 +745,9 @@ class Classes(set): """ def __init__(self, element): - """Initialise the Classes set for the given element.""" + """ + Initialise the CSS Classes set for the given element. + """ self._class_list = element._dom_element.classList super().__init__(self._class_list) @@ -759,26 +764,40 @@ class Classes(set): ) def add(self, class_name): - """Add a class.""" + """ + Add a CSS class. + """ for name in self._extract_class_names(class_name): super().add(name) self._class_list.add(name) def remove(self, class_name): - """Remove a class.""" + """ + Remove a CSS class. + + Will log a warning if the class is not present, but will not raise an + error. + """ for name in self._extract_class_names(class_name): - super().remove(name) - self._class_list.remove(name) + if name in self: + super().remove(name) + self._class_list.remove(name) + else: + console.warn(f"Class '{name}' not found in element classes.") def discard(self, class_name): - """Remove a class if present.""" + """ + Remove a CSS class if present. + """ for name in self._extract_class_names(class_name): super().discard(name) if name in self._class_list: self._class_list.remove(name) def clear(self): - """Remove all classes.""" + """ + Remove all CSS classes. + """ super().clear() while self._class_list.length > 0: self._class_list.remove(self._class_list.item(0)) diff --git a/core/tests/python/tests/test_web.py b/core/tests/python/tests/test_web.py index cfbdf936..84ce6a9a 100644 --- a/core/tests/python/tests/test_web.py +++ b/core/tests/python/tests/test_web.py @@ -337,6 +337,10 @@ class TestElement: assert div.classes == {"class1", "class2", "class3"} div.classes.remove("class2 class3") assert div.classes == {"class1"} + # Remove the final class + div.classes.remove("class1") + # Removing a non-existent class should not raise an error. + div.classes.remove("non-existent-class") async def test_when_decorator(self): called = False