@pyscript/core 0.7.10 → 0.7.12

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 (80) hide show
  1. package/dist/{codemirror-7vXPINKi.js → codemirror-WbVPJuAs.js} +2 -2
  2. package/dist/codemirror-WbVPJuAs.js.map +1 -0
  3. package/dist/{codemirror_commands-CN4gxvZk.js → codemirror_commands-BRu2f-2p.js} +2 -2
  4. package/dist/codemirror_commands-BRu2f-2p.js.map +1 -0
  5. package/dist/codemirror_lang-python-q-wuh0nL.js +2 -0
  6. package/dist/codemirror_lang-python-q-wuh0nL.js.map +1 -0
  7. package/dist/codemirror_language-DqPeLcFN.js +2 -0
  8. package/dist/codemirror_language-DqPeLcFN.js.map +1 -0
  9. package/dist/{codemirror_state-BIAL8JKm.js → codemirror_state-DWQh5Ruf.js} +2 -2
  10. package/dist/codemirror_state-DWQh5Ruf.js.map +1 -0
  11. package/dist/codemirror_view-CMXZSWgf.js +2 -0
  12. package/dist/codemirror_view-CMXZSWgf.js.map +1 -0
  13. package/dist/core-DmpFMpAn.js +4 -0
  14. package/dist/core-DmpFMpAn.js.map +1 -0
  15. package/dist/core.js +1 -1
  16. package/dist/{deprecations-manager-qW023Rjf.js → deprecations-manager-HQLYNCYn.js} +2 -2
  17. package/dist/{deprecations-manager-qW023Rjf.js.map → deprecations-manager-HQLYNCYn.js.map} +1 -1
  18. package/dist/{donkey-7fH6-q0I.js → donkey-B3F8VdSV.js} +2 -2
  19. package/dist/{donkey-7fH6-q0I.js.map → donkey-B3F8VdSV.js.map} +1 -1
  20. package/dist/{error-CO7OuWCh.js → error-DS_5_C5_.js} +2 -2
  21. package/dist/{error-CO7OuWCh.js.map → error-DS_5_C5_.js.map} +1 -1
  22. package/dist/index-DaYI1YXo.js +2 -0
  23. package/dist/index-DaYI1YXo.js.map +1 -0
  24. package/dist/{mpy-BZxQ23WL.js → mpy-Bo2uW6nt.js} +2 -2
  25. package/dist/{mpy-BZxQ23WL.js.map → mpy-Bo2uW6nt.js.map} +1 -1
  26. package/dist/{py-DI_TP8Id.js → py-BEf8j7L5.js} +2 -2
  27. package/dist/{py-DI_TP8Id.js.map → py-BEf8j7L5.js.map} +1 -1
  28. package/dist/{py-editor-BJBbMtNv.js → py-editor-C0XF2rwE.js} +2 -2
  29. package/dist/{py-editor-BJBbMtNv.js.map → py-editor-C0XF2rwE.js.map} +1 -1
  30. package/dist/{py-game-C7N5JK0D.js → py-game-eWNz96mt.js} +2 -2
  31. package/dist/{py-game-C7N5JK0D.js.map → py-game-eWNz96mt.js.map} +1 -1
  32. package/dist/{py-terminal-Cy8siD6F.js → py-terminal-VtavPj1S.js} +2 -2
  33. package/dist/{py-terminal-Cy8siD6F.js.map → py-terminal-VtavPj1S.js.map} +1 -1
  34. package/dist/xterm_addon-fit-DxKdSnof.js +14 -0
  35. package/dist/xterm_addon-fit-DxKdSnof.js.map +1 -0
  36. package/dist/xterm_addon-web-links-B6rWzrcs.js +14 -0
  37. package/dist/xterm_addon-web-links-B6rWzrcs.js.map +1 -0
  38. package/dist/zip-CgZGjqjF.js +2 -0
  39. package/dist/zip-CgZGjqjF.js.map +1 -0
  40. package/package.json +16 -15
  41. package/src/3rd-party/xterm_addon-fit.js +14 -2
  42. package/src/3rd-party/xterm_addon-web-links.js +14 -2
  43. package/src/core.js +13 -2
  44. package/src/stdlib/pyscript/__init__.py +100 -31
  45. package/src/stdlib/pyscript/context.py +198 -0
  46. package/src/stdlib/pyscript/display.py +211 -127
  47. package/src/stdlib/pyscript/events.py +191 -88
  48. package/src/stdlib/pyscript/fetch.py +156 -25
  49. package/src/stdlib/pyscript/ffi.py +132 -16
  50. package/src/stdlib/pyscript/flatted.py +78 -1
  51. package/src/stdlib/pyscript/fs.py +207 -50
  52. package/src/stdlib/pyscript/media.py +210 -50
  53. package/src/stdlib/pyscript/storage.py +214 -27
  54. package/src/stdlib/pyscript/util.py +28 -7
  55. package/src/stdlib/pyscript/web.py +1079 -881
  56. package/src/stdlib/pyscript/websocket.py +252 -45
  57. package/src/stdlib/pyscript/workers.py +176 -27
  58. package/src/stdlib/pyscript.js +13 -13
  59. package/src/sync.js +1 -1
  60. package/types/stdlib/pyscript.d.ts +1 -1
  61. package/dist/codemirror-7vXPINKi.js.map +0 -1
  62. package/dist/codemirror_commands-CN4gxvZk.js.map +0 -1
  63. package/dist/codemirror_lang-python-CkOVBHci.js +0 -2
  64. package/dist/codemirror_lang-python-CkOVBHci.js.map +0 -1
  65. package/dist/codemirror_language-DOkvasqm.js +0 -2
  66. package/dist/codemirror_language-DOkvasqm.js.map +0 -1
  67. package/dist/codemirror_state-BIAL8JKm.js.map +0 -1
  68. package/dist/codemirror_view-Bt4sLgyA.js +0 -2
  69. package/dist/codemirror_view-Bt4sLgyA.js.map +0 -1
  70. package/dist/core-5ORB_Mcj.js +0 -4
  71. package/dist/core-5ORB_Mcj.js.map +0 -1
  72. package/dist/index-jZ1aOVVJ.js +0 -2
  73. package/dist/index-jZ1aOVVJ.js.map +0 -1
  74. package/dist/xterm_addon-fit--gyF3PcZ.js +0 -2
  75. package/dist/xterm_addon-fit--gyF3PcZ.js.map +0 -1
  76. package/dist/xterm_addon-web-links-D95xh2la.js +0 -2
  77. package/dist/xterm_addon-web-links-D95xh2la.js.map +0 -1
  78. package/dist/zip-CakRHzZu.js +0 -2
  79. package/dist/zip-CakRHzZu.js.map +0 -1
  80. package/src/stdlib/pyscript/magic_js.py +0 -84
@@ -1,64 +1,104 @@
1
+ """
2
+ Display Pythonic content in the browser.
3
+
4
+ This module provides the `display()` function for rendering Python objects
5
+ in the web page. The function introspects objects to determine the appropriate
6
+ [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types/Common_types)
7
+ and rendering method.
8
+
9
+ Supported MIME types:
10
+
11
+ - `text/plain`: Plain text (HTML-escaped)
12
+ - `text/html`: HTML content
13
+ - `image/png`: PNG images as data URLs
14
+ - `image/jpeg`: JPEG images as data URLs
15
+ - `image/svg+xml`: SVG graphics
16
+ - `application/json`: JSON data
17
+ - `application/javascript`: JavaScript code (discouraged)
18
+
19
+ The `display()` function uses standard Python representation methods
20
+ (`_repr_html_`, `_repr_png_`, etc.) to determine how to render objects.
21
+ Objects can provide a `_repr_mimebundle_` method to specify preferred formats
22
+ like this:
23
+
24
+ ```python
25
+ def _repr_mimebundle_(self):
26
+ return {
27
+ "text/html": "<b>Bold HTML</b>",
28
+ "image/png": "<base64-encoded-png-data>",
29
+ }
30
+ ```
31
+
32
+ Heavily inspired by
33
+ [IPython's rich display system](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html).
34
+ """
35
+
1
36
  import base64
2
37
  import html
3
38
  import io
4
- import re
5
-
6
- from pyscript.magic_js import current_target, document, window
39
+ from collections import OrderedDict
40
+ from pyscript.context import current_target, document, window
7
41
  from pyscript.ffi import is_none
8
42
 
9
- _MIME_METHODS = {
10
- "savefig": "image/png",
11
- "_repr_javascript_": "application/javascript",
12
- "_repr_json_": "application/json",
13
- "_repr_latex": "text/latex",
14
- "_repr_png_": "image/png",
15
- "_repr_jpeg_": "image/jpeg",
16
- "_repr_pdf_": "application/pdf",
17
- "_repr_svg_": "image/svg+xml",
18
- "_repr_markdown_": "text/markdown",
19
- "_repr_html_": "text/html",
20
- "__repr__": "text/plain",
21
- }
22
-
23
43
 
24
44
  def _render_image(mime, value, meta):
25
- # If the image value is using bytes we should convert it to base64
26
- # otherwise it will return raw bytes and the browser will not be able to
27
- # render it.
45
+ """
46
+ Render image (`mime`) data (`value`) as an HTML img element with data URL.
47
+ Any `meta` attributes are added to the img tag.
48
+
49
+ Accepts both raw bytes and base64-encoded strings for flexibility. This
50
+ only handles PNG and JPEG images. SVG images are handled separately as
51
+ their raw XML content (which the browser can render directly).
52
+ """
28
53
  if isinstance(value, bytes):
29
54
  value = base64.b64encode(value).decode("utf-8")
30
-
31
- # This is the pattern of base64 strings
32
- base64_pattern = re.compile(
33
- r"^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$"
34
- )
35
- # If value doesn't match the base64 pattern we should encode it to base64
36
- if len(value) > 0 and not base64_pattern.match(value):
37
- value = base64.b64encode(value.encode("utf-8")).decode("utf-8")
38
-
39
- data = f"data:{mime};charset=utf-8;base64,{value}"
40
- attrs = " ".join(['{k}="{v}"' for k, v in meta.items()])
41
- return f'<img src="{data}" {attrs}></img>'
55
+ attrs = "".join([f' {k}="{v}"' for k, v in meta.items()])
56
+ return f'<img src="data:{mime};base64,{value}"{attrs}>'
42
57
 
43
58
 
44
- def _identity(value, meta):
45
- return value
59
+ # Maps MIME types to rendering functions.
60
+ _MIME_TO_RENDERERS = {
61
+ "text/plain": lambda v, m: html.escape(v),
62
+ "text/html": lambda v, m: v,
63
+ "image/png": lambda v, m: _render_image("image/png", v, m),
64
+ "image/jpeg": lambda v, m: _render_image("image/jpeg", v, m),
65
+ "image/svg+xml": lambda v, m: v,
66
+ "application/json": lambda v, m: v,
67
+ "application/javascript": lambda v, m: f"<script>{v}<\\/script>",
68
+ }
46
69
 
47
70
 
48
- _MIME_RENDERERS = {
49
- "text/plain": html.escape,
50
- "text/html": _identity,
51
- "image/png": lambda value, meta: _render_image("image/png", value, meta),
52
- "image/jpeg": lambda value, meta: _render_image("image/jpeg", value, meta),
53
- "image/svg+xml": _identity,
54
- "application/json": _identity,
55
- "application/javascript": lambda value, meta: f"<script>{value}<\\/script>",
56
- }
71
+ # Maps Python representation methods to MIME types. This is an ordered dict
72
+ # because the order defines preference when multiple methods are available,
73
+ # and MicroPython's limited dicts don't preserve insertion order.
74
+ _METHOD_TO_MIME = OrderedDict(
75
+ [
76
+ ("savefig", "image/png"),
77
+ ("_repr_png_", "image/png"),
78
+ ("_repr_jpeg_", "image/jpeg"),
79
+ ("_repr_svg_", "image/svg+xml"),
80
+ ("_repr_html_", "text/html"),
81
+ ("_repr_json_", "application/json"),
82
+ ("_repr_javascript_", "application/javascript"),
83
+ ("__repr__", "text/plain"),
84
+ ]
85
+ )
57
86
 
58
87
 
59
88
  class HTML:
60
89
  """
61
- Wrap a string so that display() can render it as plain HTML
90
+ Wrap a string to render as unescaped HTML in `display()`. This is
91
+ necessary because plain strings are automatically HTML-escaped for safety:
92
+
93
+ ```python
94
+ from pyscript import HTML, display
95
+
96
+
97
+ display(HTML("<h1>Hello World</h1>"))
98
+ ```
99
+
100
+ Inspired by
101
+ [`IPython.display.HTML`](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#IPython.display.HTML).
62
102
  """
63
103
 
64
104
  def __init__(self, html):
@@ -68,112 +108,156 @@ class HTML:
68
108
  return self._html
69
109
 
70
110
 
71
- def _eval_formatter(obj, print_method):
111
+ def _get_representation(obj, method):
72
112
  """
73
- Evaluates a formatter method.
113
+ Call the given representation `method` on an object (`obj`).
114
+
115
+ Handles special cases like matplotlib's `savefig`. Returns `None`
116
+ if the `method` doesn't exist.
74
117
  """
75
- if print_method == "__repr__":
118
+ if method == "__repr__":
76
119
  return repr(obj)
77
- if hasattr(obj, print_method):
78
- if print_method == "savefig":
79
- buf = io.BytesIO()
80
- obj.savefig(buf, format="png")
81
- buf.seek(0)
82
- return base64.b64encode(buf.read()).decode("utf-8")
83
- return getattr(obj, print_method)()
84
- if print_method == "_repr_mimebundle_":
85
- return {}, {}
86
- return None
87
-
88
-
89
- def _format_mime(obj):
120
+ if not hasattr(obj, method):
121
+ return None
122
+ if method == "savefig":
123
+ buf = io.BytesIO()
124
+ obj.savefig(buf, format="png")
125
+ buf.seek(0)
126
+ return base64.b64encode(buf.read()).decode("utf-8")
127
+ return getattr(obj, method)()
128
+
129
+
130
+ def _get_content_and_mime(obj):
90
131
  """
91
- Formats object using _repr_x_ methods.
132
+ Returns the formatted raw content to be inserted into the DOM representing
133
+ the given object, along with the object's detected MIME type.
134
+
135
+ Returns a tuple of (html_string, mime_type).
136
+
137
+ Prefers _repr_mimebundle_ if available, otherwise tries individual
138
+ representation methods, falling back to __repr__ (with a warning in
139
+ the console).
140
+
141
+ Implements a subset of IPython's rich display system (mimebundle support,
142
+ etc...).
92
143
  """
93
144
  if isinstance(obj, str):
94
145
  return html.escape(obj), "text/plain"
95
-
96
- mimebundle = _eval_formatter(obj, "_repr_mimebundle_")
97
- if isinstance(mimebundle, tuple):
98
- format_dict, _ = mimebundle
99
- else:
100
- format_dict = mimebundle
101
-
102
- output, not_available = None, []
103
- for method, mime_type in _MIME_METHODS.items():
104
- if mime_type in format_dict:
105
- output = format_dict[mime_type]
146
+ # Prefer an object's mimebundle.
147
+ mimebundle = _get_representation(obj, "_repr_mimebundle_")
148
+ if mimebundle:
149
+ if isinstance(mimebundle, tuple):
150
+ # Grab global metadata.
151
+ format_dict, global_meta = mimebundle
106
152
  else:
107
- output = _eval_formatter(obj, method)
108
-
109
- if is_none(output):
153
+ format_dict, global_meta = mimebundle, {}
154
+ # Try to render using mimebundle formats.
155
+ for mime_type, output in format_dict.items():
156
+ if mime_type in _MIME_TO_RENDERERS:
157
+ meta = global_meta.get(mime_type, {})
158
+ # If output is a tuple, merge format-specific metadata.
159
+ if isinstance(output, tuple):
160
+ output, format_meta = output
161
+ meta.update(format_meta)
162
+ return _MIME_TO_RENDERERS[mime_type](output, meta), mime_type
163
+ # No mimebundle or no available renderers therein, so try individual
164
+ # methods.
165
+ for method, mime_type in _METHOD_TO_MIME.items():
166
+ if mime_type not in _MIME_TO_RENDERERS:
110
167
  continue
111
- if mime_type not in _MIME_RENDERERS:
112
- not_available.append(mime_type)
168
+ output = _get_representation(obj, method)
169
+ if output is None:
113
170
  continue
114
- break
115
- if is_none(output):
116
- if not_available:
117
- window.console.warn(
118
- f"Rendered object requested unavailable MIME renderers: {not_available}"
119
- )
120
- output = repr(output)
121
- mime_type = "text/plain"
122
- elif isinstance(output, tuple):
123
- output, meta = output
124
- else:
125
171
  meta = {}
126
- return _MIME_RENDERERS[mime_type](output, meta), mime_type
172
+ if isinstance(output, tuple):
173
+ output, meta = output
174
+ return _MIME_TO_RENDERERS[mime_type](output, meta), mime_type
175
+ # Ultimate fallback to repr with warning.
176
+ window.console.warn(
177
+ f"Object {type(obj).__name__} has no supported representation method. "
178
+ "Using __repr__ as fallback."
179
+ )
180
+ output = repr(obj)
181
+ return html.escape(output), "text/plain"
127
182
 
128
183
 
129
- def _write(element, value, append=False):
130
- html, mime_type = _format_mime(value)
131
- if html == "\\n":
132
- return
184
+ def _write_to_dom(element, value, append):
185
+ """
186
+ Given an `element` and a `value`, write formatted content to the referenced
187
+ DOM element. If `append` is True, content is added to the existing content;
188
+ otherwise, the existing content is replaced.
133
189
 
190
+ Creates a wrapper `div` when appending multiple items to preserve
191
+ structure.
192
+ """
193
+ html_content, mime_type = _get_content_and_mime(value)
194
+ if not html_content.strip():
195
+ return
134
196
  if append:
135
- out_element = document.createElement("div")
136
- element.append(out_element)
197
+ container = document.createElement("div")
198
+ element.append(container)
137
199
  else:
138
- out_element = element.lastElementChild
139
- if is_none(out_element):
140
- out_element = element
141
-
200
+ container = element
142
201
  if mime_type in ("application/javascript", "text/html"):
143
- script_element = document.createRange().createContextualFragment(html)
144
- out_element.append(script_element)
202
+ container.append(document.createRange().createContextualFragment(html_content))
145
203
  else:
146
- out_element.innerHTML = html
204
+ container.innerHTML = html_content
147
205
 
148
206
 
149
207
  def display(*values, target=None, append=True):
150
- if is_none(target):
151
- target = current_target()
152
- elif not isinstance(target, str):
153
- msg = f"target must be str or None, not {target.__class__.__name__}"
154
- raise TypeError(msg)
155
- elif target == "":
156
- msg = "Cannot have an empty target"
157
- raise ValueError(msg)
158
- elif target.startswith("#"):
159
- # note: here target is str and not None!
160
- # align with @when behavior
161
- target = target[1:]
208
+ """
209
+ Display Python objects in the web page.
162
210
 
163
- element = document.getElementById(target)
211
+ * `*values`: Python objects to display. Each object is introspected to
212
+ determine the appropriate rendering method.
213
+ * `target`: DOM element ID where content should be displayed. If `None`
214
+ (default), uses the current script tag's designated output area. This
215
+ can start with '#' (which will be stripped for compatibility).
216
+ * `append`: If `True` (default), add content to existing output. If
217
+ `False`, replace existing content before displaying.
164
218
 
165
- # If target cannot be found on the page, a ValueError is raised
166
- if is_none(element):
167
- msg = f"Invalid selector with id={target}. Cannot be found in the page."
168
- raise ValueError(msg)
219
+ When used in a worker, `display()` requires an explicit `target` parameter
220
+ to identify where content will be displayed. If used on the main thread,
221
+ it automatically uses the current `<script>` tag as the target. If the
222
+ script tag has a `target` attribute, that element will be used instead.
223
+
224
+ A ValueError is raised if a valid target cannot be found for the current
225
+ context.
226
+
227
+ ```python
228
+ from pyscript import display, HTML
229
+
230
+
231
+ # Display raw HTML.
232
+ display(HTML("<h1>Hello, World!</h1>"))
233
+
234
+ # Display in current script's output area.
235
+ display("Hello, World!")
236
+
237
+ # Display in a specific element.
238
+ display("Hello", target="my-div")
169
239
 
170
- # if element is a <script type="py">, it has a 'target' attribute which
171
- # points to the visual element holding the displayed values. In that case,
172
- # use that.
240
+ # Replace existing content (note the `#`).
241
+ display("New content", target="#my-div", append=False)
242
+
243
+ # Display multiple values in the default target.
244
+ display("First", "Second", "Third")
245
+ ```
246
+ """
247
+ if isinstance(target, str):
248
+ # There's a valid target.
249
+ target = target[1:] if target.startswith("#") else target
250
+ elif is_none(target):
251
+ target = current_target()
252
+ element = document.getElementById(target)
253
+ if is_none(element):
254
+ raise ValueError(f"Cannot find element with id='{target}' in the page.")
255
+ # If possible, use a script tag's target attribute.
173
256
  if element.tagName == "SCRIPT" and hasattr(element, "target"):
174
257
  element = element.target
175
-
176
- for v in values:
177
- if not append:
178
- element.replaceChildren()
179
- _write(element, v, append=append)
258
+ # Clear before displaying all values when not appending.
259
+ if not append:
260
+ element.replaceChildren()
261
+ # Add each value.
262
+ for value in values:
263
+ _write_to_dom(element, value, append)