@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,87 +1,247 @@
1
+ """
2
+ This module provides classes and functions for interacting with
3
+ [media devices and streams](https://developer.mozilla.org/en-US/docs/Web/API/Media_Capture_and_Streams_API)
4
+ in the browser, enabling you to work with cameras, microphones,
5
+ and other media input/output devices directly from Python.
6
+
7
+ Use this module for:
8
+
9
+ - Accessing webcams for video capture.
10
+ - Recording audio from microphones.
11
+ - Enumerating available media devices.
12
+ - Applying constraints to media streams (resolution, frame rate, etc.).
13
+
14
+ ```python
15
+ from pyscript import document
16
+ from pyscript.media import Device, list_devices
17
+
18
+
19
+ # Get a video stream from the default camera.
20
+ stream = await Device.request_stream(video=True)
21
+
22
+ # Display in a video element.
23
+ video = document.getElementById("my-video")
24
+ video.srcObject = stream
25
+
26
+ # Or list all available devices.
27
+ devices = await list_devices()
28
+ for device in devices:
29
+ print(f"{device.kind}: {device.label}")
30
+ ```
31
+
32
+ Using media devices requires user permission. Browsers will show a
33
+ permission dialog when accessing devices for the first time.
34
+ """
35
+
1
36
  from pyscript import window
2
37
  from pyscript.ffi import to_js
3
38
 
4
39
 
5
40
  class Device:
6
- """Device represents a media input or output device, such as a microphone,
7
- camera, or headset.
41
+ """
42
+ Represents a media input or output device.
43
+
44
+ This class wraps a browser
45
+ [MediaDeviceInfo object](https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo),
46
+ providing Pythonic access to device properties like `ID`, `label`, and
47
+ `kind` (audio/video, input/output).
48
+
49
+ Devices are typically obtained via the `list_devices()` function in this
50
+ module, rather than constructed directly.
51
+
52
+ ```python
53
+ from pyscript.media import list_devices
54
+
55
+
56
+ # Get all available devices.
57
+ devices = await list_devices()
58
+
59
+ # Find video input devices (cameras).
60
+ cameras = [d for d in devices if d.kind == "videoinput"]
61
+
62
+ # Get a stream from a specific camera.
63
+ if cameras:
64
+ stream = await cameras[0].get_stream()
65
+ ```
8
66
  """
9
67
 
10
68
  def __init__(self, device):
11
- self._dom_element = device
69
+ """
70
+ Create a Device wrapper around a MediaDeviceInfo `device`.
71
+ """
72
+ self._device_info = device
12
73
 
13
74
  @property
14
75
  def id(self):
15
- return self._dom_element.deviceId
76
+ """
77
+ Unique identifier for this device.
78
+
79
+ This `ID` persists across sessions but is reset when the user clears
80
+ cookies. It's unique to the origin of the calling application.
81
+ """
82
+ return self._device_info.deviceId
16
83
 
17
84
  @property
18
85
  def group(self):
19
- return self._dom_element.groupId
86
+ """
87
+ Group identifier for related devices.
88
+
89
+ Devices belonging to the same physical device (e.g., a monitor with
90
+ both a camera and microphone) share the same `group ID`.
91
+ """
92
+ return self._device_info.groupId
20
93
 
21
94
  @property
22
95
  def kind(self):
23
- return self._dom_element.kind
96
+ """
97
+ Device type: `"videoinput"`, `"audioinput"`, or `"audiooutput"`.
98
+ """
99
+ return self._device_info.kind
24
100
 
25
101
  @property
26
102
  def label(self):
27
- return self._dom_element.label
103
+ """
104
+ Human-readable description of the device.
105
+
106
+ Example: `"External USB Webcam"` or `"Built-in Microphone"`.
107
+ """
108
+ return self._device_info.label
28
109
 
29
110
  def __getitem__(self, key):
111
+ """
112
+ Support bracket notation for JavaScript interop.
113
+
114
+ Allows accessing properties via `device["id"]` syntax. Necessary
115
+ when Device instances are proxied to JavaScript.
116
+ """
30
117
  return getattr(self, key)
31
118
 
32
119
  @classmethod
33
- async def load(cls, audio=False, video=True):
120
+ async def request_stream(cls, audio=False, video=True):
34
121
  """
35
- Load the device stream.
122
+ Request a media stream with the specified constraints.
123
+
124
+ This is a class method that requests access to media devices matching
125
+ the given `audio` and `video` constraints. The browser will prompt the
126
+ user for permission if needed and return a `MediaStream` object that
127
+ can be assigned to video/audio elements.
128
+
129
+ Simple boolean constraints for `audio` and `video` can be used to
130
+ request default devices. More complex constraints can be specified as
131
+ dictionaries conforming to
132
+ [the MediaTrackConstraints interface](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints).
133
+
134
+ ```python
135
+ from pyscript import document
136
+ from pyscript.media import Device
137
+
138
+
139
+ # Get default video stream.
140
+ stream = await Device.request_stream()
141
+
142
+ # Get stream with specific constraints.
143
+ stream = await Device.request_stream(
144
+ video={"width": 1920, "height": 1080}
145
+ )
146
+
147
+ # Get audio and video.
148
+ stream = await Device.request_stream(audio=True, video=True)
149
+
150
+ # Use the stream.
151
+ video_el = document.getElementById("camera")
152
+ video_el.srcObject = stream
153
+ ```
154
+
155
+ This method will trigger a browser permission dialog on first use.
36
156
  """
37
157
  options = {}
38
- options["audio"] = audio
158
+ if isinstance(audio, bool):
159
+ options["audio"] = audio
160
+ elif isinstance(audio, dict):
161
+ # audio is a dict of constraints (sampleRate, echoCancellation etc...).
162
+ options["audio"] = audio
39
163
  if isinstance(video, bool):
40
164
  options["video"] = video
41
- else:
42
- options["video"] = {}
43
- for k in video:
44
- options["video"][k] = video[k]
165
+ elif isinstance(video, dict):
166
+ # video is a dict of constraints (width, height etc...).
167
+ options["video"] = video
45
168
  return await window.navigator.mediaDevices.getUserMedia(to_js(options))
46
169
 
170
+ @classmethod
171
+ async def load(cls, audio=False, video=True):
172
+ """
173
+ !!! warning
174
+ **Deprecated: Use `request_stream()` instead.**
175
+
176
+ This method is retained for backwards compatibility but will be
177
+ removed in a future release. Please use `request_stream()` instead.
178
+ """
179
+ return await cls.request_stream(audio=audio, video=video)
180
+
47
181
  async def get_stream(self):
48
- key = self.kind.replace("input", "").replace("output", "")
49
- options = {key: {"deviceId": {"exact": self.id}}}
50
- return await self.load(**options)
182
+ """
183
+ Get a media stream from this specific device.
184
+
185
+ ```python
186
+ from pyscript.media import list_devices
187
+
188
+
189
+ # List all devices.
190
+ devices = await list_devices()
191
+
192
+ # Find a specific camera.
193
+ my_camera = None
194
+ for device in devices:
195
+ if device.kind == "videoinput" and "USB" in device.label:
196
+ my_camera = device
197
+ break
198
+
199
+ # Get a stream from that specific camera.
200
+ if my_camera:
201
+ stream = await my_camera.get_stream()
202
+ ```
203
+
204
+ This will trigger a permission dialog if the user hasn't already
205
+ granted permission for this device type.
206
+ """
207
+ # Extract media type from device kind (e.g., "videoinput" -> "video").
208
+ media_type = self.kind.replace("input", "").replace("output", "")
209
+ # Request stream with exact device ID constraint.
210
+ options = {media_type: {"deviceId": {"exact": self.id}}}
211
+ return await self.request_stream(**options)
51
212
 
52
213
 
53
- async def list_devices() -> list[dict]:
214
+ async def list_devices():
54
215
  """
55
- Return the list of the currently available media input and output devices,
56
- such as microphones, cameras, headsets, and so forth.
57
-
58
- Output:
59
-
60
- list(dict) - list of dictionaries representing the available media devices.
61
- Each dictionary has the following keys:
62
- * deviceId: a string that is an identifier for the represented device
63
- that is persisted across sessions. It is un-guessable by other
64
- applications and unique to the origin of the calling application.
65
- It is reset when the user clears cookies (for Private Browsing, a
66
- different identifier is used that is not persisted across sessions).
67
-
68
- * groupId: a string that is a group identifier. Two devices have the same
69
- group identifier if they belong to the same physical device — for
70
- example a monitor with both a built-in camera and a microphone.
71
-
72
- * kind: an enumerated value that is either "videoinput", "audioinput"
73
- or "audiooutput".
74
-
75
- * label: a string describing this device (for example "External USB
76
- Webcam").
77
-
78
- Note: the returned list will omit any devices that are blocked by the document
79
- Permission Policy: microphone, camera, speaker-selection (for output devices),
80
- and so on. Access to particular non-default devices is also gated by the
81
- Permissions API, and the list will omit devices for which the user has not
82
- granted explicit permission.
216
+ Returns a list of all media devices currently available to the browser,
217
+ such as microphones, cameras, and speakers.
218
+
219
+ ```python
220
+ from pyscript.media import list_devices
221
+
222
+
223
+ # Get all devices.
224
+ devices = await list_devices()
225
+
226
+ # Print device information.
227
+ for device in devices:
228
+ print(f"{device.kind}: {device.label} (ID: {device.id})")
229
+
230
+ # Filter for specific device types.
231
+ cameras = [d for d in devices if d.kind == "videoinput"]
232
+ microphones = [d for d in devices if d.kind == "audioinput"]
233
+ speakers = [d for d in devices if d.kind == "audiooutput"]
234
+ ```
235
+
236
+ The returned list will omit devices that are blocked by the document
237
+ [Permission Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Permissions_Policy)
238
+ (microphone, camera, speaker-selection) or for
239
+ which the user has not granted explicit permission.
240
+
241
+ For security and privacy, device labels may be empty strings until
242
+ permission is granted. See
243
+ [this document](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices)
244
+ for more information about this web standard.
83
245
  """
84
- # https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices
85
- return [
86
- Device(obj) for obj in await window.navigator.mediaDevices.enumerateDevices()
87
- ]
246
+ device_infos = await window.navigator.mediaDevices.enumerateDevices()
247
+ return [Device(device_info) for device_info in device_infos]
@@ -1,11 +1,69 @@
1
- from polyscript import storage as _storage
1
+ """
2
+ This module wraps the browser's
3
+ [IndexedDB persistent storage](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
4
+ to provide a familiar Python dictionary API. Data is automatically
5
+ serialized and persisted, surviving page reloads and browser restarts.
6
+
7
+ Storage is persistent per origin (domain), isolated between different sites
8
+ for security. Browsers typically allow each origin to store up to 10-60% of
9
+ total disk space, depending on browser and configuration.
10
+
11
+ What this module provides:
12
+
13
+ - A `dict`-like API (get, set, delete, iterate).
14
+ - Automatic serialization of common Python types.
15
+ - Background persistence with optional explicit `sync()`.
16
+ - Support for custom `Storage` subclasses.
17
+
18
+ ```python
19
+ from pyscript import storage
20
+
21
+
22
+ # Create or open a named storage.
23
+ my_data = await storage("user-preferences")
24
+
25
+ # Use like a regular dictionary.
26
+ my_data["theme"] = "dark"
27
+ my_data["font_size"] = 14
28
+ my_data["settings"] = {"notifications": True, "sound": False}
29
+
30
+ # Changes are queued automatically.
31
+ # To ensure immediate write, sync explicitly.
32
+ await my_data.sync()
33
+
34
+ # Read values (survives page reload).
35
+ theme = my_data.get("theme", "light")
36
+ ```
37
+
38
+ Common types are automatically serialized: `bool`, `int`, `float`, `str`, `None`,
39
+ `list`, `dict`, `tuple`. Binary data (`bytearray`, `memoryview`) can be stored as
40
+ single values but not nested in structures.
41
+
42
+ Tuples are deserialized as lists due to IndexedDB limitations.
43
+
44
+ !!! info
45
+ Browsers typically allow 10-60% of total disk space per origin. Chrome
46
+ and Edge allow up to 60%, Firefox up to 10 GiB (or 10% of disk, whichever
47
+ is smaller). Safari varies by app type. These limits are unlikely to be
48
+ reached in typical usage.
49
+ """
50
+
51
+ from polyscript import storage as _polyscript_storage
2
52
  from pyscript.flatted import parse as _parse
3
53
  from pyscript.flatted import stringify as _stringify
4
54
  from pyscript.ffi import is_none
5
55
 
6
56
 
7
- # convert a Python value into an IndexedDB compatible entry
8
- def _to_idb(value):
57
+ def _convert_to_idb(value):
58
+ """
59
+ Convert a Python `value` to an IndexedDB-compatible format.
60
+
61
+ Values are serialized using Flatted (for circular reference support)
62
+ with type information to enable proper deserialization. It returns a
63
+ JSON string representing the serialized value.
64
+
65
+ Will raise a TypeError if the value type is not supported.
66
+ """
9
67
  if is_none(value):
10
68
  return _stringify(["null", 0])
11
69
  if isinstance(value, (bool, float, int, str, list, dict, tuple)):
@@ -14,50 +72,179 @@ def _to_idb(value):
14
72
  return _stringify(["bytearray", list(value)])
15
73
  if isinstance(value, memoryview):
16
74
  return _stringify(["memoryview", list(value)])
17
- msg = f"Unexpected value: {value}"
18
- raise TypeError(msg)
75
+ raise TypeError(f"Cannot serialize type {type(value).__name__} for storage.")
19
76
 
20
77
 
21
- # convert an IndexedDB compatible entry into a Python value
22
- def _from_idb(value):
23
- (
24
- kind,
25
- result,
26
- ) = _parse(value)
78
+ def _convert_from_idb(value):
79
+ """
80
+ Convert an IndexedDB `value` back to its Python representation.
81
+
82
+ Uses type information stored during serialization to reconstruct the
83
+ original Python type.
84
+ """
85
+ kind, data = _parse(value)
86
+
27
87
  if kind == "null":
28
88
  return None
29
89
  if kind == "generic":
30
- return result
90
+ return data
31
91
  if kind == "bytearray":
32
- return bytearray(result)
92
+ return bytearray(data)
33
93
  if kind == "memoryview":
34
- return memoryview(bytearray(result))
94
+ return memoryview(bytearray(data))
95
+ # Fallback for all other types.
35
96
  return value
36
97
 
37
98
 
38
99
  class Storage(dict):
100
+ """
101
+ A persistent dictionary backed by the browser's IndexedDB.
102
+
103
+ This class provides a dict-like interface with automatic persistence.
104
+ Changes are queued for background writing, with optional explicit
105
+ synchronization via `sync()`.
106
+
107
+ Inherits from `dict`, so all standard dictionary methods work as expected.
108
+
109
+ ```python
110
+ from pyscript import storage
111
+
112
+
113
+ # Open a storage.
114
+ prefs = await storage("preferences")
115
+
116
+ # Use as a dictionary.
117
+ prefs["color"] = "blue"
118
+ prefs["count"] = 42
119
+
120
+ # Iterate like a dict.
121
+ for key, value in prefs.items():
122
+ print(f"{key}: {value}")
123
+
124
+ # Ensure writes complete immediately.
125
+ await prefs.sync()
126
+ ```
127
+
128
+ Sometimes you may need to subclass `Storage` to add custom behavior:
129
+
130
+ ```python
131
+ from pyscript import storage, Storage, window
132
+
133
+
134
+ class LoggingStorage(Storage):
135
+ def __setitem__(self, key, value):
136
+ window.console.log(f"Setting {key} = {value}")
137
+ super().__setitem__(key, value)
138
+
139
+ my_store = await storage("app-data", storage_class=LoggingStorage)
140
+ my_store["test"] = 123 # Logs to console.
141
+ ```
142
+ """
143
+
39
144
  def __init__(self, store):
40
- super().__init__({k: _from_idb(v) for k, v in store.entries()})
41
- self.__store__ = store
145
+ """
146
+ Create a Storage instance wrapping an IndexedDB `store` (a JS
147
+ proxy).
148
+ """
149
+ super().__init__(
150
+ {key: _convert_from_idb(value) for key, value in store.entries()}
151
+ )
152
+ self._store = store
153
+
154
+ def __delitem__(self, key):
155
+ """
156
+ Delete an item from storage via its `key`.
157
+
158
+ The deletion is queued for persistence. Use `sync()` to ensure
159
+ immediate completion.
160
+ """
161
+ self._store.delete(key)
162
+ super().__delitem__(key)
42
163
 
43
- def __delitem__(self, attr):
44
- self.__store__.delete(attr)
45
- super().__delitem__(attr)
164
+ def __setitem__(self, key, value):
165
+ """
166
+ Set a `key` to a `value` in storage.
46
167
 
47
- def __setitem__(self, attr, value):
48
- self.__store__.set(attr, _to_idb(value))
49
- super().__setitem__(attr, value)
168
+ The change is queued for persistence. Use `sync()` to ensure
169
+ immediate completion. The `value` must be a supported type for
170
+ serialization.
171
+ """
172
+ self._store.set(key, _convert_to_idb(value))
173
+ super().__setitem__(key, value)
50
174
 
51
175
  def clear(self):
52
- self.__store__.clear()
176
+ """
177
+ Remove all items from storage.
178
+
179
+ The `clear()` operation is queued for persistence. Use `sync()` to ensure
180
+ immediate completion.
181
+ """
182
+ self._store.clear()
53
183
  super().clear()
54
184
 
55
185
  async def sync(self):
56
- await self.__store__.sync()
186
+ """
187
+ Force immediate synchronization to IndexedDB.
188
+
189
+ By default, storage operations are queued and written asynchronously.
190
+ Call `sync()` when you need to guarantee changes are persisted immediately,
191
+ such as before critical operations or page unload.
192
+
193
+ ```python
194
+ store = await storage("important-data")
195
+ store["critical_value"] = data
196
+
197
+ # Ensure it's written before proceeding.
198
+ await store.sync()
199
+ ```
200
+
201
+ This is a blocking operation that waits for IndexedDB to complete
202
+ the write.
203
+ """
204
+ await self._store.sync()
57
205
 
58
206
 
59
207
  async def storage(name="", storage_class=Storage):
208
+ """
209
+ Open or create persistent storage with a unique `name` and optional
210
+ `storage_class` (used to extend the default `Storage` based behavior).
211
+
212
+ Each storage is isolated by name within the current origin (domain).
213
+ If the storage doesn't exist, it will be created. If it does exist,
214
+ its current contents will be loaded.
215
+
216
+ This function returns a `Storage` instance (or custom subclass instance)
217
+ acting as a persistent dictionary. A `ValueError` is raised if `name` is
218
+ empty or not provided.
219
+
220
+ ```python
221
+ from pyscript import storage
222
+
223
+
224
+ # Basic usage.
225
+ user_data = await storage("user-profile")
226
+ user_data["name"] = "Alice"
227
+ user_data["age"] = 30
228
+
229
+ # Multiple independent storages.
230
+ settings = await storage("app-settings")
231
+ cache = await storage("api-cache")
232
+
233
+ # With custom Storage class.
234
+ class ValidatingStorage(Storage):
235
+ def __setitem__(self, key, value):
236
+ if not isinstance(key, str):
237
+ raise TypeError("Keys must be strings")
238
+ super().__setitem__(key, value)
239
+
240
+ validated = await storage("validated-data", ValidatingStorage)
241
+ ```
242
+
243
+ Storage names are automatically prefixed with `"@pyscript/"` to
244
+ namespace them within IndexedDB.
245
+ """
60
246
  if not name:
61
- msg = "The storage name must be defined"
62
- raise ValueError(msg)
63
- return storage_class(await _storage(f"@pyscript/{name}"))
247
+ raise ValueError("Storage name must be a non-empty string")
248
+
249
+ underlying_store = await _polyscript_storage(f"@pyscript/{name}")
250
+ return storage_class(underlying_store)
@@ -1,11 +1,24 @@
1
+ """
2
+ This module contains general-purpose utility functions that don't fit into
3
+ more specific modules. These utilities handle cross-platform compatibility
4
+ between Pyodide and MicroPython, feature detection, and common type
5
+ conversions:
6
+
7
+ - `as_bytearray`: Convert JavaScript `ArrayBuffer` to Python `bytearray`.
8
+ - `NotSupported`: Placeholder for unavailable features in specific contexts.
9
+ - `is_awaitable`: Detect `async` functions across Python implementations.
10
+
11
+ These utilities are primarily used internally by PyScript but are available
12
+ for use in application code when needed.
13
+ """
14
+
1
15
  import js
2
- import sys
3
16
  import inspect
4
17
 
5
18
 
6
19
  def as_bytearray(buffer):
7
20
  """
8
- Given a JavaScript ArrayBuffer, convert it to a Python bytearray in a
21
+ Given a JavaScript `ArrayBuffer`, convert it to a Python `bytearray` in a
9
22
  MicroPython friendly manner.
10
23
  """
11
24
  ui8a = js.Uint8Array.new(buffer)
@@ -42,17 +55,25 @@ class NotSupported:
42
55
  def is_awaitable(obj):
43
56
  """
44
57
  Returns a boolean indication if the passed in obj is an awaitable
45
- function. (MicroPython treats awaitables as generator functions, and if
46
- the object is a closure containing an async function we need to work
47
- carefully.)
58
+ function. This is interpreter agnostic.
59
+
60
+ !!! info
61
+ MicroPython treats awaitables as generator functions, and if
62
+ the object is a closure containing an async function or a bound method
63
+ we need to work carefully.
48
64
  """
49
65
  from pyscript import config
50
66
 
51
- if config["type"] == "mpy": # Is MicroPython?
67
+ if config["type"] == "mpy":
52
68
  # MicroPython doesn't appear to have a way to determine if a closure is
53
69
  # an async function except via the repr. This is a bit hacky.
54
- if "<closure <generator>" in repr(obj):
70
+ r = repr(obj)
71
+ if "<closure <generator>" in r:
72
+ return True
73
+ # Same applies to bound methods.
74
+ if "<bound_method" in r and "<generator>" in r:
55
75
  return True
76
+ # In MicroPython, generator functions are awaitable.
56
77
  return inspect.isgeneratorfunction(obj)
57
78
 
58
79
  return inspect.iscoroutinefunction(obj)