pyinkcli 0.1.0__py3-none-any.whl

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 (145) hide show
  1. pyinkcli/__init__.py +59 -0
  2. pyinkcli/_component_runtime.py +284 -0
  3. pyinkcli/_restore_cursor.py +39 -0
  4. pyinkcli/_suspense_runtime.py +127 -0
  5. pyinkcli/_yoga.py +501 -0
  6. pyinkcli/ansi_tokenizer.py +323 -0
  7. pyinkcli/colorize.py +157 -0
  8. pyinkcli/component.py +10 -0
  9. pyinkcli/components/AccessibilityContext.py +5 -0
  10. pyinkcli/components/App.py +170 -0
  11. pyinkcli/components/AppContext.py +5 -0
  12. pyinkcli/components/BackgroundContext.py +5 -0
  13. pyinkcli/components/Box.py +89 -0
  14. pyinkcli/components/CursorContext.py +30 -0
  15. pyinkcli/components/ErrorBoundary.py +35 -0
  16. pyinkcli/components/ErrorOverview.py +50 -0
  17. pyinkcli/components/FocusContext.py +40 -0
  18. pyinkcli/components/Newline.py +8 -0
  19. pyinkcli/components/Spacer.py +12 -0
  20. pyinkcli/components/Static.py +67 -0
  21. pyinkcli/components/StderrContext.py +37 -0
  22. pyinkcli/components/StdinContext.py +44 -0
  23. pyinkcli/components/StdoutContext.py +37 -0
  24. pyinkcli/components/Text.py +121 -0
  25. pyinkcli/components/Transform.py +15 -0
  26. pyinkcli/components/__init__.py +29 -0
  27. pyinkcli/components/_accessibility_runtime.py +26 -0
  28. pyinkcli/components/_app_context_runtime.py +45 -0
  29. pyinkcli/components/_background_runtime.py +28 -0
  30. pyinkcli/cursor_helpers.py +116 -0
  31. pyinkcli/dom.py +41 -0
  32. pyinkcli/get_max_width.py +18 -0
  33. pyinkcli/hooks/__init__.py +37 -0
  34. pyinkcli/hooks/_runtime.py +653 -0
  35. pyinkcli/hooks/state.py +30 -0
  36. pyinkcli/hooks/use_app.py +99 -0
  37. pyinkcli/hooks/use_box_metrics.py +80 -0
  38. pyinkcli/hooks/use_cursor.py +98 -0
  39. pyinkcli/hooks/use_focus.py +241 -0
  40. pyinkcli/hooks/use_focus_manager.py +60 -0
  41. pyinkcli/hooks/use_input.py +204 -0
  42. pyinkcli/hooks/use_is_screen_reader_enabled.py +27 -0
  43. pyinkcli/hooks/use_paste.py +52 -0
  44. pyinkcli/hooks/use_stderr.py +88 -0
  45. pyinkcli/hooks/use_stdin.py +198 -0
  46. pyinkcli/hooks/use_stdout.py +146 -0
  47. pyinkcli/hooks/use_window_size.py +32 -0
  48. pyinkcli/index.py +53 -0
  49. pyinkcli/ink.py +1090 -0
  50. pyinkcli/input_parser.py +272 -0
  51. pyinkcli/instances.py +7 -0
  52. pyinkcli/kitty_keyboard.py +30 -0
  53. pyinkcli/log_update.py +285 -0
  54. pyinkcli/measure_element.py +58 -0
  55. pyinkcli/measure_text.py +34 -0
  56. pyinkcli/output.py +5 -0
  57. pyinkcli/packages/__init__.py +2 -0
  58. pyinkcli/packages/ink/__init__.py +68 -0
  59. pyinkcli/packages/ink/dom.py +413 -0
  60. pyinkcli/packages/ink/host_config.py +22 -0
  61. pyinkcli/packages/ink/output.py +669 -0
  62. pyinkcli/packages/ink/render_background.py +41 -0
  63. pyinkcli/packages/ink/render_border.py +92 -0
  64. pyinkcli/packages/ink/render_node_to_output.py +285 -0
  65. pyinkcli/packages/ink/renderer.py +75 -0
  66. pyinkcli/packages/ink/styles.py +474 -0
  67. pyinkcli/packages/react_devtools_core/__init__.py +35 -0
  68. pyinkcli/packages/react_devtools_core/backend.py +3 -0
  69. pyinkcli/packages/react_devtools_core/backend_constants.py +10 -0
  70. pyinkcli/packages/react_devtools_core/backend_handlers.py +494 -0
  71. pyinkcli/packages/react_devtools_core/backend_inspection.py +319 -0
  72. pyinkcli/packages/react_devtools_core/hydration.py +1787 -0
  73. pyinkcli/packages/react_devtools_core/src/__init__.py +2 -0
  74. pyinkcli/packages/react_devtools_core/src/backend.py +365 -0
  75. pyinkcli/packages/react_devtools_core/src/editor.py +164 -0
  76. pyinkcli/packages/react_devtools_core/src/standalone.py +166 -0
  77. pyinkcli/packages/react_devtools_core/standalone.py +3 -0
  78. pyinkcli/packages/react_devtools_core/window_polyfill.py +28 -0
  79. pyinkcli/packages/react_reconciler/ReactChildFiber.py +451 -0
  80. pyinkcli/packages/react_reconciler/ReactEventPriorities.py +14 -0
  81. pyinkcli/packages/react_reconciler/ReactFiberClassComponent.py +421 -0
  82. pyinkcli/packages/react_reconciler/ReactFiberCommitWork.py +54 -0
  83. pyinkcli/packages/react_reconciler/ReactFiberComponentStack.py +102 -0
  84. pyinkcli/packages/react_reconciler/ReactFiberConfig.py +213 -0
  85. pyinkcli/packages/react_reconciler/ReactFiberContainerUpdate.py +286 -0
  86. pyinkcli/packages/react_reconciler/ReactFiberDevToolsCommands.py +572 -0
  87. pyinkcli/packages/react_reconciler/ReactFiberDevToolsHook.py +81 -0
  88. pyinkcli/packages/react_reconciler/ReactFiberDevToolsInspection.py +1285 -0
  89. pyinkcli/packages/react_reconciler/ReactFiberHostContext.py +29 -0
  90. pyinkcli/packages/react_reconciler/ReactFiberReconciler.py +108 -0
  91. pyinkcli/packages/react_reconciler/ReactFiberReconcilerClassComponent.py +182 -0
  92. pyinkcli/packages/react_reconciler/ReactFiberReconcilerCommands.py +293 -0
  93. pyinkcli/packages/react_reconciler/ReactFiberReconcilerDevTools.py +26 -0
  94. pyinkcli/packages/react_reconciler/ReactFiberReconcilerFacade.py +48 -0
  95. pyinkcli/packages/react_reconciler/ReactFiberReconcilerInspection.py +326 -0
  96. pyinkcli/packages/react_reconciler/ReactFiberReconcilerMutation.py +150 -0
  97. pyinkcli/packages/react_reconciler/ReactFiberReconcilerRoot.py +118 -0
  98. pyinkcli/packages/react_reconciler/ReactFiberReconcilerState.py +153 -0
  99. pyinkcli/packages/react_reconciler/ReactFiberReconcilerWorkLoop.py +70 -0
  100. pyinkcli/packages/react_reconciler/ReactFiberRoot.py +28 -0
  101. pyinkcli/packages/react_reconciler/ReactFiberTreeReflection.py +179 -0
  102. pyinkcli/packages/react_reconciler/ReactFiberWorkLoop.py +86 -0
  103. pyinkcli/packages/react_reconciler/ReactReconcilerConstants.py +9 -0
  104. pyinkcli/packages/react_reconciler/__init__.py +25 -0
  105. pyinkcli/packages/react_reconciler/constants.py +3 -0
  106. pyinkcli/packages/react_reconciler/index.py +3 -0
  107. pyinkcli/packages/react_reconciler/reconciler.py +33 -0
  108. pyinkcli/packages/react_reconciler/reflection.py +3 -0
  109. pyinkcli/packages/react_router/__init__.py +158 -0
  110. pyinkcli/packages/react_router/actions.py +113 -0
  111. pyinkcli/packages/react_router/components.py +317 -0
  112. pyinkcli/packages/react_router/context.py +114 -0
  113. pyinkcli/packages/react_router/errors.py +112 -0
  114. pyinkcli/packages/react_router/hooks.py +266 -0
  115. pyinkcli/packages/react_router/href.py +55 -0
  116. pyinkcli/packages/react_router/router/__init__.py +105 -0
  117. pyinkcli/packages/react_router/router/history.py +234 -0
  118. pyinkcli/packages/react_router/router/utils.py +933 -0
  119. pyinkcli/parse_keypress.py +499 -0
  120. pyinkcli/patch_console.py +89 -0
  121. pyinkcli/py.typed +0 -0
  122. pyinkcli/reconciler.py +4 -0
  123. pyinkcli/render.py +30 -0
  124. pyinkcli/render_background.py +5 -0
  125. pyinkcli/render_border.py +5 -0
  126. pyinkcli/render_node_to_output.py +20 -0
  127. pyinkcli/render_to_string.py +106 -0
  128. pyinkcli/renderer.py +5 -0
  129. pyinkcli/sanitize_ansi.py +44 -0
  130. pyinkcli/squash_text_nodes.py +12 -0
  131. pyinkcli/styles.py +5 -0
  132. pyinkcli/suspense_runtime.py +21 -0
  133. pyinkcli/utils/__init__.py +99 -0
  134. pyinkcli/utils/ansi_escapes.py +227 -0
  135. pyinkcli/utils/cli_boxes.py +135 -0
  136. pyinkcli/utils/string_width.py +93 -0
  137. pyinkcli/utils/wrap_ansi.py +338 -0
  138. pyinkcli/utils.py +23 -0
  139. pyinkcli/wrap_text.py +52 -0
  140. pyinkcli/write_synchronized.py +23 -0
  141. pyinkcli/yoga_compat.py +117 -0
  142. pyinkcli-0.1.0.dist-info/METADATA +95 -0
  143. pyinkcli-0.1.0.dist-info/RECORD +145 -0
  144. pyinkcli-0.1.0.dist-info/WHEEL +4 -0
  145. pyinkcli-0.1.0.dist-info/licenses/LICENSE +10 -0
pyinkcli/__init__.py ADDED
@@ -0,0 +1,59 @@
1
+ """pyinkcli public package surface."""
2
+
3
+ from pyinkcli.index import (
4
+ DOMElement,
5
+ Box,
6
+ Key,
7
+ Newline,
8
+ Spacer,
9
+ Static,
10
+ Text,
11
+ Transform,
12
+ kittyFlags,
13
+ kittyModifiers,
14
+ measureElement,
15
+ render,
16
+ renderToString,
17
+ useApp,
18
+ useBoxMetrics,
19
+ useCursor,
20
+ useFocus,
21
+ useFocusManager,
22
+ useInput,
23
+ useIsScreenReaderEnabled,
24
+ usePaste,
25
+ useStderr,
26
+ useStdin,
27
+ useStdout,
28
+ useWindowSize,
29
+ )
30
+
31
+ __version__ = "0.1.0"
32
+
33
+ __all__ = [
34
+ "render",
35
+ "renderToString",
36
+ "Box",
37
+ "Text",
38
+ "Static",
39
+ "Transform",
40
+ "Newline",
41
+ "Spacer",
42
+ "Key",
43
+ "useInput",
44
+ "usePaste",
45
+ "useApp",
46
+ "useStdin",
47
+ "useStdout",
48
+ "useStderr",
49
+ "useFocus",
50
+ "useFocusManager",
51
+ "useIsScreenReaderEnabled",
52
+ "useCursor",
53
+ "useWindowSize",
54
+ "useBoxMetrics",
55
+ "measureElement",
56
+ "DOMElement",
57
+ "kittyFlags",
58
+ "kittyModifiers",
59
+ ]
@@ -0,0 +1,284 @@
1
+ """
2
+ Internal component runtime for pyinkcli.
3
+
4
+ This module holds the actual element/component implementation.
5
+ Public compatibility imports remain in `component.py`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from contextlib import AbstractContextManager
11
+ from typing import Any, Callable, Dict, List, Optional, Union
12
+
13
+
14
+ class _Element:
15
+ __slots__ = ("type", "props", "children", "key")
16
+ pass
17
+
18
+
19
+ class _ScopedNode:
20
+ __slots__ = ("node", "context_manager_factories")
21
+
22
+ def __getattr__(self, name: str) -> Any:
23
+ return getattr(self.node, name)
24
+
25
+
26
+ RenderableNode = Union["_Element", "_ScopedNode", str, None]
27
+
28
+
29
+ def _normalize_props(props: Optional[Dict[str, Any]]) -> Dict[str, Any]:
30
+ return props or {}
31
+
32
+
33
+ def _create_element_record(
34
+ type: Union[str, Callable, type],
35
+ props: Optional[Dict[str, Any]],
36
+ children: List[RenderableNode],
37
+ key: Optional[str],
38
+ ) -> "_Element":
39
+ element = object.__new__(_Element)
40
+ element.type = type
41
+ element.props = _normalize_props(props)
42
+ element.children = children
43
+ element.key = key
44
+ return element
45
+
46
+
47
+ def _is_element_record(value: Any) -> bool:
48
+ return isinstance(value, _Element)
49
+
50
+
51
+ def _is_scoped_node(value: Any) -> bool:
52
+ return isinstance(value, _ScopedNode)
53
+
54
+
55
+ def _is_renderable_node(value: Any) -> bool:
56
+ return _is_element_record(value) or _is_scoped_node(value) or isinstance(value, str)
57
+
58
+
59
+ def _is_text_renderable(value: Any) -> bool:
60
+ return isinstance(value, str)
61
+
62
+
63
+ def _is_render_component_passthrough(value: Any) -> bool:
64
+ return _is_text_renderable(value) or isElement(value)
65
+
66
+
67
+ def _normalize_child(child: Any) -> RenderableNode:
68
+ if child is None:
69
+ return None
70
+ if _is_renderable_node(child):
71
+ return child
72
+ return str(child)
73
+
74
+
75
+ def _normalize_children(children: Any) -> List[RenderableNode]:
76
+ processed_children: List[RenderableNode] = []
77
+ for child in children:
78
+ if isinstance(child, (list, tuple)):
79
+ for subchild in child:
80
+ normalized = _normalize_child(subchild)
81
+ if normalized is not None:
82
+ processed_children.append(normalized)
83
+ continue
84
+
85
+ normalized = _normalize_child(child)
86
+ if normalized is not None:
87
+ processed_children.append(normalized)
88
+
89
+ return processed_children
90
+
91
+
92
+ def _coerce_render_result(result: Any) -> RenderableNode:
93
+ if _is_component_instance(result):
94
+ return _coerce_render_result(result.render())
95
+ if _is_renderable_node(result):
96
+ return result
97
+ if result is None:
98
+ return None
99
+ return str(result)
100
+
101
+
102
+ def _is_component_instance(value: Any) -> bool:
103
+ return isinstance(value, _Component)
104
+
105
+
106
+ def _is_component_class(value: Any) -> bool:
107
+ return isinstance(value, type) and issubclass(value, _Component)
108
+
109
+
110
+ def _coalesce_component_children(
111
+ children: tuple[RenderableNode, ...],
112
+ ) -> RenderableNode:
113
+ normalized_children = _normalize_children(children)
114
+ if not normalized_children:
115
+ return None
116
+ if len(normalized_children) == 1:
117
+ return normalized_children[0]
118
+ return _fragment(*normalized_children)
119
+
120
+
121
+ def _merge_component_props(
122
+ children: tuple[RenderableNode, ...],
123
+ props: Dict[str, Any],
124
+ ) -> Dict[str, Any]:
125
+ merged_props = dict(props)
126
+ if "children" not in merged_props:
127
+ merged_props["children"] = _coalesce_component_children(children)
128
+ return merged_props
129
+
130
+
131
+ def _create_component_instance(
132
+ component: type["_Component"],
133
+ children: tuple[RenderableNode, ...],
134
+ props: Dict[str, Any],
135
+ ) -> "_Component":
136
+ return component(**_merge_component_props(children, props))
137
+
138
+
139
+ def _invoke_component(
140
+ component: Callable,
141
+ children: tuple[RenderableNode, ...],
142
+ props: Dict[str, Any],
143
+ ) -> Any:
144
+ if _is_component_class(component):
145
+ instance = _create_component_instance(component, children, props)
146
+ return instance.render()
147
+ return component(*children, **props)
148
+
149
+
150
+ def createElement(
151
+ type: Union[str, Callable, type],
152
+ *children: RenderableNode,
153
+ key: Optional[str] = None,
154
+ **props: Any,
155
+ ) -> RenderableNode:
156
+ return _create_element_record(
157
+ type=type,
158
+ props=props,
159
+ children=_normalize_children(children),
160
+ key=key,
161
+ )
162
+
163
+
164
+ def scopeRender(
165
+ node: RenderableNode,
166
+ *context_manager_factories: Callable[[], AbstractContextManager[Any]],
167
+ ) -> RenderableNode:
168
+ scoped = object.__new__(_ScopedNode)
169
+ scoped.node = node
170
+ scoped.context_manager_factories = tuple(context_manager_factories)
171
+ return scoped
172
+
173
+
174
+ class _Component:
175
+ def __init__(self, **props: Any):
176
+ self.props = props
177
+ self.state: Dict[str, Any] = {}
178
+ self._state_version = 0
179
+ self._is_unmounted = False
180
+ self._is_mounted = False
181
+ self._last_rendered_node: RenderableNode = None
182
+ self._pending_previous_state: Optional[Dict[str, Any]] = None
183
+ self._nearest_error_boundary: Optional["_Component"] = None
184
+
185
+ def render(self) -> RenderableNode:
186
+ return None
187
+
188
+ def set_state(
189
+ self,
190
+ update: Optional[
191
+ Union[
192
+ Dict[str, Any],
193
+ Callable[[Dict[str, Any], Dict[str, Any]], Optional[Dict[str, Any]]],
194
+ ]
195
+ ] = None,
196
+ **kwargs: Any,
197
+ ) -> None:
198
+ if self._is_unmounted:
199
+ return
200
+
201
+ partial_state: Dict[str, Any] = {}
202
+ if callable(update):
203
+ computed = update(dict(self.state), dict(self.props))
204
+ if isinstance(computed, dict):
205
+ partial_state.update(computed)
206
+ elif isinstance(update, dict):
207
+ partial_state.update(update)
208
+
209
+ if kwargs:
210
+ partial_state.update(kwargs)
211
+
212
+ if not partial_state:
213
+ return
214
+
215
+ if self._pending_previous_state is None:
216
+ self._pending_previous_state = dict(self.state)
217
+ self.state.update(partial_state)
218
+ self._state_version += 1
219
+ from pyinkcli.hooks._runtime import _request_rerender
220
+
221
+ _request_rerender()
222
+
223
+ def force_update(self) -> None:
224
+ if self._is_unmounted:
225
+ return
226
+ from pyinkcli.hooks._runtime import _request_rerender
227
+
228
+ _request_rerender()
229
+
230
+
231
+ def component(
232
+ func: Optional[Callable] = None, *, name: Optional[str] = None
233
+ ) -> Callable:
234
+ def wrapper(fn: Callable) -> Callable:
235
+ fn._is_component = True
236
+ fn._component_name = name or fn.__name__
237
+ return fn
238
+
239
+ if func is not None:
240
+ return wrapper(func)
241
+ return wrapper
242
+
243
+
244
+ def is_component(obj: Any) -> bool:
245
+ return callable(obj) and obj is not _Fragment
246
+
247
+
248
+ def isElement(obj: Any) -> bool:
249
+ return _is_element_record(obj) or _is_scoped_node(obj)
250
+
251
+
252
+ def renderComponent(
253
+ component: Union[Callable, _Component, RenderableNode],
254
+ *children: RenderableNode,
255
+ **props: Any,
256
+ ) -> RenderableNode:
257
+ if component is None:
258
+ return None
259
+
260
+ if _is_render_component_passthrough(component):
261
+ return component
262
+
263
+ if _is_component_instance(component):
264
+ return _coerce_render_result(component.render())
265
+
266
+ if _is_component_class(component):
267
+ instance = _create_component_instance(component, children, props)
268
+ return _coerce_render_result(instance.render())
269
+
270
+ if callable(component):
271
+ return _coerce_render_result(_invoke_component(component, children, props))
272
+
273
+ return str(component)
274
+
275
+
276
+ class _Fragment:
277
+ pass
278
+
279
+
280
+ def _fragment(*children: RenderableNode) -> RenderableNode:
281
+ return createElement(_Fragment, *children)
282
+
283
+
284
+ __all__ = ["createElement", "component", "isElement", "RenderableNode", "scopeRender"]
@@ -0,0 +1,39 @@
1
+ """Process-exit cursor restore parity with JS cli-cursor/restore-cursor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import atexit
6
+ import sys
7
+
8
+ from pyinkcli.cursor_helpers import showCursorEscape
9
+
10
+ _registered = False
11
+
12
+
13
+ def _restore_cursor_on_exit() -> None:
14
+ stream = sys.stderr
15
+ is_tty = stream.isatty() if hasattr(stream, "isatty") else False
16
+ if not is_tty:
17
+ return
18
+
19
+ try:
20
+ writer = getattr(stream, "raw_write", None) or getattr(stream, "write", None)
21
+ if callable(writer):
22
+ writer(showCursorEscape)
23
+ flush = getattr(stream, "flush", None)
24
+ if callable(flush):
25
+ flush()
26
+ except Exception:
27
+ pass
28
+
29
+
30
+ def ensureCursorRestoreRegistered() -> None:
31
+ global _registered
32
+ if _registered:
33
+ return
34
+
35
+ atexit.register(_restore_cursor_on_exit)
36
+ _registered = True
37
+
38
+
39
+ __all__ = ["ensureCursorRestoreRegistered"]
@@ -0,0 +1,127 @@
1
+ """Internal suspense resource runtime for pyinkcli."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, Callable, Hashable
8
+
9
+ from pyinkcli.hooks._runtime import _has_rerender_target, _request_rerender
10
+
11
+
12
+ class SuspendSignal(Exception):
13
+ def __init__(self, key: Hashable):
14
+ super().__init__(f"Suspended on resource {key!r}")
15
+ self.key = key
16
+
17
+
18
+ @dataclass
19
+ class _ResourceRecord:
20
+ status: str = "pending"
21
+ value: Any = None
22
+ error: Exception | None = None
23
+ started: bool = False
24
+ lock: threading.Lock = field(default_factory=threading.Lock)
25
+
26
+
27
+ _records: dict[Hashable, _ResourceRecord] = {}
28
+ _records_lock = threading.Lock()
29
+
30
+
31
+ def _resolve_resource(
32
+ key: Hashable,
33
+ record: _ResourceRecord,
34
+ loader: Callable[[], Any],
35
+ ) -> None:
36
+ try:
37
+ value = loader()
38
+ except Exception as error: # pragma: no cover
39
+ with _records_lock:
40
+ current = _records.get(key)
41
+ if current is not record:
42
+ return
43
+ with record.lock:
44
+ record.status = "rejected"
45
+ record.error = error
46
+ else:
47
+ with _records_lock:
48
+ current = _records.get(key)
49
+ if current is not record:
50
+ return
51
+ with record.lock:
52
+ record.status = "resolved"
53
+ record.value = value
54
+
55
+ if _has_rerender_target():
56
+ _request_rerender()
57
+
58
+
59
+ def readResource(key: Hashable, loader: Callable[[], Any]) -> Any:
60
+ with _records_lock:
61
+ record = _records.get(key)
62
+ if record is None:
63
+ record = _ResourceRecord()
64
+ _records[key] = record
65
+
66
+ with record.lock:
67
+ if record.status == "resolved":
68
+ return record.value
69
+ if record.status == "rejected":
70
+ raise record.error # type: ignore[misc]
71
+ if not record.started:
72
+ record.started = True
73
+ threading.Thread(
74
+ target=_resolve_resource,
75
+ args=(key, record, loader),
76
+ daemon=True,
77
+ ).start()
78
+
79
+ raise SuspendSignal(key)
80
+
81
+
82
+ def preloadResource(key: Hashable, loader: Callable[[], Any]) -> None:
83
+ with _records_lock:
84
+ record = _records.get(key)
85
+ if record is None:
86
+ record = _ResourceRecord()
87
+ _records[key] = record
88
+
89
+ with record.lock:
90
+ if record.status in {"resolved", "rejected"} or record.started:
91
+ return
92
+ record.started = True
93
+ threading.Thread(
94
+ target=_resolve_resource,
95
+ args=(key, record, loader),
96
+ daemon=True,
97
+ ).start()
98
+
99
+
100
+ def peekResource(key: Hashable) -> Any:
101
+ with _records_lock:
102
+ record = _records.get(key)
103
+ if record is None:
104
+ return None
105
+
106
+ with record.lock:
107
+ if record.status == "resolved":
108
+ return record.value
109
+ if record.status == "rejected":
110
+ raise record.error # type: ignore[misc]
111
+
112
+ return None
113
+
114
+
115
+ def invalidateResource(key: Hashable) -> None:
116
+ with _records_lock:
117
+ _records.pop(key, None)
118
+
119
+
120
+ def resetResource(key: Hashable) -> None:
121
+ with _records_lock:
122
+ _records.pop(key, None)
123
+
124
+
125
+ def resetAllResources() -> None:
126
+ with _records_lock:
127
+ _records.clear()