@pyscript/core 0.4.56 → 0.5.0-rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/{codemirror-BZEh__gm.js → codemirror-ZWX__zRS.js} +2 -2
  2. package/dist/{codemirror-BZEh__gm.js.map → codemirror-ZWX__zRS.js.map} +1 -1
  3. package/dist/{codemirror_commands-leH8shna.js → codemirror_commands-BvofQ4B2.js} +2 -2
  4. package/dist/{codemirror_commands-leH8shna.js.map → codemirror_commands-BvofQ4B2.js.map} +1 -1
  5. package/dist/{codemirror_lang-python-DuOzopOD.js → codemirror_lang-python-CAjFAwEr.js} +2 -2
  6. package/dist/{codemirror_lang-python-DuOzopOD.js.map → codemirror_lang-python-CAjFAwEr.js.map} +1 -1
  7. package/dist/{codemirror_language-Dakzaxks.js → codemirror_language-BzugrQDC.js} +2 -2
  8. package/dist/{codemirror_language-Dakzaxks.js.map → codemirror_language-BzugrQDC.js.map} +1 -1
  9. package/dist/{codemirror_view-Bm5_2vT5.js → codemirror_view-DrrFKRyn.js} +2 -2
  10. package/dist/codemirror_view-DrrFKRyn.js.map +1 -0
  11. package/dist/core-jHCq6GXZ.js +2 -0
  12. package/dist/core-jHCq6GXZ.js.map +1 -0
  13. package/dist/core.js +1 -1
  14. package/dist/{deprecations-manager-BYGF73-y.js → deprecations-manager-JLWNpJ-L.js} +2 -2
  15. package/dist/{deprecations-manager-BYGF73-y.js.map → deprecations-manager-JLWNpJ-L.js.map} +1 -1
  16. package/dist/{error-DgYfWJQg.js → error-BZdmXuMQ.js} +2 -2
  17. package/dist/{error-DgYfWJQg.js.map → error-BZdmXuMQ.js.map} +1 -1
  18. package/dist/{index-DA5aljNV.js → index-YJCpGJSa.js} +2 -2
  19. package/dist/{index-DA5aljNV.js.map → index-YJCpGJSa.js.map} +1 -1
  20. package/dist/{mpy-DiqSc6w9.js → mpy-CA-8GLzg.js} +2 -2
  21. package/dist/{mpy-DiqSc6w9.js.map → mpy-CA-8GLzg.js.map} +1 -1
  22. package/dist/{py-editor-DHmqLMO5.js → py-editor-BFwOcA7b.js} +2 -2
  23. package/dist/{py-editor-DHmqLMO5.js.map → py-editor-BFwOcA7b.js.map} +1 -1
  24. package/dist/{py-terminal-E6BBcU6N.js → py-terminal-GLATgrHP.js} +2 -2
  25. package/dist/{py-terminal-E6BBcU6N.js.map → py-terminal-GLATgrHP.js.map} +1 -1
  26. package/dist/{py-DEubHlb_.js → py-tfDQPNNm.js} +2 -2
  27. package/dist/{py-DEubHlb_.js.map → py-tfDQPNNm.js.map} +1 -1
  28. package/package.json +6 -6
  29. package/src/stdlib/pyscript/magic_js.py +4 -12
  30. package/src/stdlib/pyscript/web/__init__.py +7 -4
  31. package/src/stdlib/pyscript/web/elements.py +134 -114
  32. package/src/stdlib/pyscript.js +3 -3
  33. package/dist/codemirror_view-Bm5_2vT5.js.map +0 -1
  34. package/dist/core-DKrwnOQh.js +0 -3
  35. package/dist/core-DKrwnOQh.js.map +0 -1
  36. package/dist.zip +0 -0
@@ -1,3 +1,4 @@
1
+ # noinspection PyPep8Naming
1
2
  import inspect
2
3
  import sys
3
4
 
@@ -34,7 +35,7 @@ def getmembers_static(cls):
34
35
 
35
36
 
36
37
  class DOMProperty:
37
- """A descriptor representing a DOM property on an Element`.
38
+ """A descriptor representing a DOM property on an `Element` instance.
38
39
 
39
40
  This maps a property on an `Element` instance, to the property with the specified
40
41
  name on the element's underlying DOM element.
@@ -53,41 +54,11 @@ class DOMProperty:
53
54
  setattr(obj._dom_element, self.name, value)
54
55
 
55
56
 
56
- def element_from_dom(dom_element):
57
- """Create an instance of the appropriate subclass of `Element` for a DOM element.
58
-
59
- If the DOM element was created via an `Element` (i.e. by us) it will have a data
60
- attribute named `data-pyscript-type` that contains the name of the subclass
61
- that created it. If the `data-pyscript-type` attribute *is* present we look up the
62
- subclass by name and create an instance of that. Otherwise, we make a 'best-guess'
63
- and look up the `Element` subclass by the DOM element's tag name (this is NOT
64
- fool-proof as many subclasses might use a `<div>`, but close enough for jazz).
65
- """
66
-
67
- # We use "getAttribute" here instead of `js_element.dataset.pyscriptType` as the
68
- # latter throws an `AttributeError` if the value isn't set. This way we just get
69
- # `None` which seems cleaner.
70
- cls_name = dom_element.getAttribute("data-pyscript-type")
71
- if cls_name:
72
- cls = ELEMENT_CLASSES_BY_NAME.get(cls_name.lower())
73
-
74
- else:
75
- cls = ELEMENT_CLASSES_BY_TAG.get(dom_element.tagName.lower())
76
-
77
- # For any unknown elements (custom tags etc.) create an instance of the 'Element'
78
- # class.
79
- if not cls:
80
- cls = Element
81
-
82
- return cls(dom_element=dom_element)
83
-
84
-
85
57
  class Element:
86
58
  tag = "div"
87
59
 
88
- # GLOBAL ATTRIBUTES.
89
60
  # These are attribute that all elements have (this list is a subset of the official
90
- # one). We are trying to capture the most used ones.
61
+ # one - we are just trying to capture the most used ones).
91
62
  accesskey = DOMProperty("accesskey")
92
63
  autofocus = DOMProperty("autofocus")
93
64
  autocapitalize = DOMProperty("autocapitalize")
@@ -105,11 +76,43 @@ class Element:
105
76
  slot = DOMProperty("slot")
106
77
  spellcheck = DOMProperty("spellcheck")
107
78
  tabindex = DOMProperty("tabindex")
108
- text = DOMProperty("textContent")
79
+ tagName = DOMProperty("tagName")
80
+ textContent = DOMProperty("textContent")
109
81
  title = DOMProperty("title")
110
82
  translate = DOMProperty("translate")
111
83
  virtualkeyboardpolicy = DOMProperty("virtualkeyboardpolicy")
112
84
 
85
+ @classmethod
86
+ def from_dom_element(cls, dom_element):
87
+ """Create an instance of the appropriate subclass of `Element` for a DOM
88
+ element.
89
+
90
+ If the DOM element was created via an `Element` (i.e. by us) it will have a data
91
+ attribute named `data-pyscript-type` that contains the name of the subclass
92
+ that created it. Hence, if the `data-pyscript-type` attribute *is* present we
93
+ look up the subclass by name and create an instance of that. Otherwise, we make
94
+ a 'best-guess' and look up the `Element` subclass by the DOM element's tag name
95
+ (this is NOT fool-proof as many subclasses might use a `<div>`, but close enough
96
+ for jazz).
97
+ """
98
+
99
+ # We use "getAttribute" here instead of `js_element.dataset.pyscriptType` as the
100
+ # latter throws an `AttributeError` if the value isn't set. This way we just get
101
+ # `None` which seems cleaner.
102
+ cls_name = dom_element.getAttribute("data-pyscript-type")
103
+ if cls_name:
104
+ element_cls = ELEMENT_CLASSES_BY_NAME.get(cls_name.lower())
105
+
106
+ else:
107
+ element_cls = ELEMENT_CLASSES_BY_TAG.get(dom_element.tagName.lower())
108
+
109
+ # For any unknown elements (custom tags etc.) create an instance of this
110
+ # class ('Element').
111
+ if not element_cls:
112
+ element_cls = cls
113
+
114
+ return element_cls(dom_element=dom_element)
115
+
113
116
  def __init__(self, dom_element=None, classes=None, style=None, **kwargs):
114
117
  """Create a new, or wrap an existing DOM element.
115
118
 
@@ -122,7 +125,7 @@ class Element:
122
125
  #
123
126
  # Using the `dataset` attribute is how you programmatically add `data-xxx`
124
127
  # attributes to a DOM element. In this case it will set an attribute that
125
- # appears in (say) the devtools as `data-pyscript-type`.
128
+ # appears in the DOM as `data-pyscript-type`.
126
129
  self._dom_element.dataset.pyscriptType = type(self).__name__
127
130
 
128
131
  self._parent = None
@@ -138,24 +141,24 @@ class Element:
138
141
  if classes:
139
142
  self.classes.add(classes)
140
143
 
141
- # Set any specified styles.
142
144
  if isinstance(style, dict):
143
145
  self.style.set(**style)
144
146
 
145
147
  elif style is not None:
146
148
  raise ValueError(
147
- f"Style should be a dictionary, received {style} (type {type(style)}) instead."
149
+ f"Style should be a dictionary, received {style} "
150
+ f"(type {type(style)}) instead."
148
151
  )
149
152
 
150
153
  self._set_dom_properties(**kwargs)
151
154
 
152
155
  def _set_dom_properties(self, **kwargs):
153
- """Set all the properties (of type DOMProperty) provided in input as properties
154
- of the class instance.
156
+ """Set the specified DOM properties.
155
157
 
156
158
  Args:
157
159
  **kwargs: The properties to set
158
160
  """
161
+ # Harvest all DOM properties from the instance's class.
159
162
  dom_properties = {
160
163
  attribute_name: attribute_value
161
164
  for attribute_name, attribute_value in getmembers_static(self.__class__)
@@ -179,7 +182,7 @@ class Element:
179
182
  @property
180
183
  def children(self):
181
184
  return ElementCollection(
182
- [element_from_dom(el) for el in self._dom_element.children]
185
+ [Element.from_dom_element(el) for el in self._dom_element.children]
183
186
  )
184
187
 
185
188
  @property
@@ -192,7 +195,7 @@ class Element:
192
195
  return self._parent
193
196
 
194
197
  if self._dom_element.parentElement:
195
- self._parent = element_from_dom(self._dom_element.parentElement)
198
+ self._parent = Element.from_dom_element(self._dom_element.parentElement)
196
199
 
197
200
  return self._parent
198
201
 
@@ -228,12 +231,13 @@ class Element:
228
231
  except AttributeError:
229
232
  # Nope! This is not an element or a NodeList.
230
233
  raise TypeError(
231
- f'Element "{child}" is a proxy object, but not a valid element or a NodeList.'
234
+ f'Element "{child}" is a proxy object, "'
235
+ f"but not a valid element or a NodeList."
232
236
  )
233
237
 
234
238
  def clone(self, clone_id=None):
235
239
  """Make a clone of the element (clones the underlying DOM object too)."""
236
- clone = element_from_dom(self._dom_element.cloneNode(True))
240
+ clone = Element.from_dom_element(self._dom_element.cloneNode(True))
237
241
  clone.id = clone_id
238
242
  return clone
239
243
 
@@ -249,8 +253,8 @@ class Element:
249
253
  """
250
254
  return ElementCollection(
251
255
  [
252
- element_from_dom(el)
253
- for el in self._dom_element.querySelectorAll(selector)
256
+ Element.from_dom_element(dom_element)
257
+ for dom_element in self._dom_element.querySelectorAll(selector)
254
258
  ]
255
259
  )
256
260
 
@@ -322,13 +326,13 @@ class Classes:
322
326
  self.remove(old_class)
323
327
  self.add(new_class)
324
328
 
325
- def toggle(self, class_name):
326
- if class_name in self:
327
- self.remove(class_name)
328
- return False
329
+ def toggle(self, *class_names):
330
+ for class_name in class_names:
331
+ if class_name in self:
332
+ self.remove(class_name)
329
333
 
330
- self.add(class_name)
331
- return True
334
+ else:
335
+ self.add(class_name)
332
336
 
333
337
 
334
338
  class HasOptions:
@@ -365,7 +369,7 @@ class Options:
365
369
  **kws,
366
370
  ) -> None:
367
371
  """Add a new option to the select element"""
368
- # create the option element and set the attributes
372
+
369
373
  option = document.createElement("option")
370
374
  if value is not None:
371
375
  kws["value"] = value
@@ -395,7 +399,9 @@ class Options:
395
399
  @property
396
400
  def options(self):
397
401
  """Return the list of options"""
398
- return [element_from_dom(opt) for opt in self._element._dom_element.options]
402
+ return [
403
+ Element.from_dom_element(opt) for opt in self._element._dom_element.options
404
+ ]
399
405
 
400
406
  @property
401
407
  def selected(self):
@@ -449,6 +455,8 @@ class Style:
449
455
 
450
456
 
451
457
  class ContainerElement(Element):
458
+ """Base class for elements that can contain other elements."""
459
+
452
460
  def __init__(
453
461
  self, *args, children=None, dom_element=None, style=None, classes=None, **kwargs
454
462
  ):
@@ -606,12 +614,15 @@ class canvas(ContainerElement):
606
614
  Output:
607
615
  None
608
616
  """
609
- link = self.create("a")
610
- link._dom_element.download = filename
611
- link._dom_element.href = self._dom_element.toDataURL()
612
- link._dom_element.click()
617
+ download_link = a(download=filename, href=self._dom_element.toDataURL())
618
+
619
+ # Adding the link to the DOM is recommended for browser compatibility to make
620
+ # sure that the click works.
621
+ self.append(download_link)
613
622
 
614
- def draw(self, what, width, height):
623
+ download_link._dom_element.click()
624
+
625
+ def draw(self, what, width=None, height=None):
615
626
  """Draw `what` on the current element
616
627
 
617
628
  Inputs:
@@ -626,7 +637,12 @@ class canvas(ContainerElement):
626
637
  what = what._dom_element
627
638
 
628
639
  # https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage
629
- self._dom_element.getContext("2d").drawImage(what, 0, 0, width, height)
640
+ ctx = self._dom_element.getContext("2d")
641
+ if width or height:
642
+ ctx.drawImage(what, 0, 0, width, height)
643
+
644
+ else:
645
+ ctx.drawImage(what, 0, 0)
630
646
 
631
647
 
632
648
  class caption(ContainerElement):
@@ -1396,6 +1412,8 @@ class video(ContainerElement):
1396
1412
  preload = DOMProperty("preload")
1397
1413
  src = DOMProperty("src")
1398
1414
  width = DOMProperty("width")
1415
+ videoHeight = DOMProperty("videoHeight")
1416
+ videoWidth = DOMProperty("videoWidth")
1399
1417
 
1400
1418
  def snap(
1401
1419
  self,
@@ -1404,48 +1422,44 @@ class video(ContainerElement):
1404
1422
  height: int | None = None,
1405
1423
  ):
1406
1424
  """
1407
- Captures a snapshot of a video.
1425
+ Capture a snapshot (i.e. a single frame) of a video to a canvas.
1408
1426
 
1409
1427
  Inputs:
1410
1428
 
1411
- * to: element where to save the snapshot of the video frame to
1412
- * width: width of the image
1413
- * height: height of the image
1429
+ * to: the canvas to save the video frame to (if None, one is created).
1430
+ * width: width of the snapshot (defaults to the video width).
1431
+ * height: height of the snapshot (defaults to the video height).
1414
1432
 
1415
1433
  Output:
1416
1434
  (Element) canvas element where the video frame snapshot was drawn into
1417
1435
  """
1436
+ width = width if width is not None else self.videoWidth
1437
+ height = height if height is not None else self.videoHeight
1438
+
1418
1439
  if to is None:
1419
- to_canvas = self.create("canvas")
1420
- if width is None:
1421
- width = self._dom_element.width
1422
- if height is None:
1423
- height = self._dom_element.height
1424
- to_canvas._dom_element.width = width
1425
- to_canvas._dom_element.height = height
1440
+ to = canvas(width=width, height=height)
1426
1441
 
1427
1442
  elif isinstance(to, Element):
1428
- if to._dom_element.tagName != "CANVAS":
1429
- raise TypeError("Element to snap to must a canvas.")
1430
- to_canvas = to
1443
+ if to.tag != "canvas":
1444
+ raise TypeError("Element to snap to must be a canvas.")
1431
1445
 
1432
1446
  elif getattr(to, "tagName", "") == "CANVAS":
1433
- to_canvas = canvas(to)
1447
+ to = canvas(dom_element=to)
1434
1448
 
1435
1449
  # If 'to' is a string, then assume it is a query selector.
1436
1450
  elif isinstance(to, str):
1437
- nodelist = document.querySelectorAll(to)
1451
+ nodelist = document.querySelectorAll(to) # NOQA
1438
1452
  if nodelist.length == 0:
1439
1453
  raise TypeError("No element with selector {to} to snap to.")
1440
1454
 
1441
1455
  if nodelist[0].tagName != "CANVAS":
1442
- raise TypeError("Element to snap to must a be canvas.")
1456
+ raise TypeError("Element to snap to must be a canvas.")
1443
1457
 
1444
- to_canvas = canvas(nodelist[0])
1458
+ to = canvas(dom_element=nodelist[0])
1445
1459
 
1446
- to_canvas.draw(self, width, height)
1460
+ to.draw(self, width, height)
1447
1461
 
1448
- return canvas
1462
+ return to
1449
1463
 
1450
1464
 
1451
1465
  class wbr(Element):
@@ -1464,7 +1478,7 @@ class grid(ContainerElement):
1464
1478
  self.style["grid-template-columns"] = layout
1465
1479
 
1466
1480
  # TODO: This should be a property
1467
- if not gap is None:
1481
+ if gap is not None:
1468
1482
  self.style["gap"] = gap
1469
1483
 
1470
1484
 
@@ -1513,9 +1527,9 @@ class ClassesCollection:
1513
1527
  for element in self._collection:
1514
1528
  element.classes.replace(old_class, new_class)
1515
1529
 
1516
- def toggle(self, class_name):
1530
+ def toggle(self, *class_names):
1517
1531
  for element in self._collection:
1518
- element.classes.toggle(class_name)
1532
+ element.classes.toggle(*class_names)
1519
1533
 
1520
1534
  def _all_class_names(self):
1521
1535
  all_class_names = set()
@@ -1554,37 +1568,8 @@ class ElementCollection:
1554
1568
  self._classes = ClassesCollection(self)
1555
1569
  self._style = StyleCollection(self)
1556
1570
 
1557
- @property
1558
- def children(self):
1559
- return self._elements
1560
-
1561
- @property
1562
- def classes(self):
1563
- return self._classes
1564
-
1565
- @property
1566
- def style(self):
1567
- return self._style
1568
-
1569
- @property
1570
- def innerHTML(self):
1571
- return self._get_attribute("innerHTML")
1572
-
1573
- @innerHTML.setter
1574
- def innerHTML(self, value):
1575
- self._set_attribute("innerHTML", value)
1576
-
1577
- @property
1578
- def value(self):
1579
- return self._get_attribute("value")
1580
-
1581
- @value.setter
1582
- def value(self, value):
1583
- self._set_attribute("value", value)
1584
-
1585
1571
  def __eq__(self, obj):
1586
- """Check if the element is the same as the other element by comparing
1587
- the underlying DOM element"""
1572
+ """Check for equality by comparing the underlying DOM elements."""
1588
1573
  return isinstance(obj, ElementCollection) and obj._elements == self._elements
1589
1574
 
1590
1575
  def __getitem__(self, key):
@@ -1598,8 +1583,7 @@ class ElementCollection:
1598
1583
  return ElementCollection(self._elements[key])
1599
1584
 
1600
1585
  # If it's anything else (basically a string) we use it as a query selector.
1601
- elements = self._elements.querySelectorAll(key)
1602
- return ElementCollection([element_from_dom(el) for el in elements])
1586
+ return self.find(key)
1603
1587
 
1604
1588
  def __iter__(self):
1605
1589
  yield from self._elements
@@ -1608,7 +1592,43 @@ class ElementCollection:
1608
1592
  return len(self._elements)
1609
1593
 
1610
1594
  def __repr__(self):
1611
- return f"{self.__class__.__name__} (length: {len(self._elements)}) {self._elements}"
1595
+ return (
1596
+ f"{self.__class__.__name__} (length: {len(self._elements)}) "
1597
+ f"{self._elements}"
1598
+ )
1599
+
1600
+ def __getattr__(self, item):
1601
+ return self._get_attribute(item)
1602
+
1603
+ def __setattr__(self, key, value):
1604
+ # This class overrides `__setattr__` to delegate "public" attributes to the
1605
+ # elements in the collection, but we don't use the usual Python pattern where we
1606
+ # set attributes on the collection itself via `self.__dict__` as it is not yet
1607
+ # supported in our build of MicroPython. Instead, we handle it here.
1608
+ if key.startswith("_"):
1609
+ super().__setattr__(key, value)
1610
+
1611
+ else:
1612
+ self._set_attribute(key, value)
1613
+
1614
+ @property
1615
+ def children(self):
1616
+ return self._elements
1617
+
1618
+ @property
1619
+ def classes(self):
1620
+ return self._classes
1621
+
1622
+ @property
1623
+ def style(self):
1624
+ return self._style
1625
+
1626
+ def find(self, selector):
1627
+ elements = []
1628
+ for element in self._elements:
1629
+ elements.extend(element.find(selector))
1630
+
1631
+ return ElementCollection(elements)
1612
1632
 
1613
1633
  def _get_attribute(self, attr, index=None):
1614
1634
  if index is None: