@ajksunkang-aios/kgraph-linux-x64 0.1.2 → 0.1.3

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 (196) hide show
  1. package/bin/kgraph-launcher +15 -3
  2. package/lib/kgraph/scripts/build-bundle.sh +17 -4
  3. package/lib/site-packages/outcome/__init__.py +20 -0
  4. package/lib/site-packages/outcome/_impl.py +239 -0
  5. package/lib/site-packages/outcome/_util.py +33 -0
  6. package/lib/site-packages/outcome/_version.py +7 -0
  7. package/lib/site-packages/outcome/py.typed +0 -0
  8. package/lib/site-packages/outcome-1.3.0.post0.dist-info/INSTALLER +1 -0
  9. package/lib/site-packages/outcome-1.3.0.post0.dist-info/LICENSE +3 -0
  10. package/lib/site-packages/outcome-1.3.0.post0.dist-info/LICENSE.APACHE2 +202 -0
  11. package/lib/site-packages/outcome-1.3.0.post0.dist-info/LICENSE.MIT +20 -0
  12. package/lib/site-packages/outcome-1.3.0.post0.dist-info/METADATA +63 -0
  13. package/lib/site-packages/outcome-1.3.0.post0.dist-info/RECORD +13 -0
  14. package/lib/site-packages/outcome-1.3.0.post0.dist-info/WHEEL +6 -0
  15. package/lib/site-packages/outcome-1.3.0.post0.dist-info/top_level.txt +1 -0
  16. package/lib/site-packages/sniffio/__init__.py +17 -0
  17. package/lib/site-packages/sniffio/_impl.py +95 -0
  18. package/lib/site-packages/sniffio/_tests/__init__.py +0 -0
  19. package/lib/site-packages/sniffio/_tests/test_sniffio.py +84 -0
  20. package/lib/site-packages/sniffio/_version.py +3 -0
  21. package/lib/site-packages/sniffio/py.typed +0 -0
  22. package/lib/site-packages/sniffio-1.3.1.dist-info/INSTALLER +1 -0
  23. package/lib/site-packages/sniffio-1.3.1.dist-info/LICENSE +3 -0
  24. package/lib/site-packages/sniffio-1.3.1.dist-info/LICENSE.APACHE2 +202 -0
  25. package/lib/site-packages/sniffio-1.3.1.dist-info/LICENSE.MIT +20 -0
  26. package/lib/site-packages/sniffio-1.3.1.dist-info/METADATA +104 -0
  27. package/lib/site-packages/sniffio-1.3.1.dist-info/RECORD +14 -0
  28. package/lib/site-packages/sniffio-1.3.1.dist-info/WHEEL +5 -0
  29. package/lib/site-packages/sniffio-1.3.1.dist-info/top_level.txt +1 -0
  30. package/lib/site-packages/sortedcontainers/__init__.py +74 -0
  31. package/lib/site-packages/sortedcontainers/sorteddict.py +812 -0
  32. package/lib/site-packages/sortedcontainers/sortedlist.py +2646 -0
  33. package/lib/site-packages/sortedcontainers/sortedset.py +733 -0
  34. package/lib/site-packages/sortedcontainers-2.4.0.dist-info/INSTALLER +1 -0
  35. package/lib/site-packages/sortedcontainers-2.4.0.dist-info/LICENSE +13 -0
  36. package/lib/site-packages/sortedcontainers-2.4.0.dist-info/METADATA +264 -0
  37. package/lib/site-packages/sortedcontainers-2.4.0.dist-info/RECORD +10 -0
  38. package/lib/site-packages/sortedcontainers-2.4.0.dist-info/WHEEL +6 -0
  39. package/lib/site-packages/sortedcontainers-2.4.0.dist-info/top_level.txt +1 -0
  40. package/lib/site-packages/trio/__init__.py +133 -0
  41. package/lib/site-packages/trio/__main__.py +3 -0
  42. package/lib/site-packages/trio/_abc.py +714 -0
  43. package/lib/site-packages/trio/_channel.py +610 -0
  44. package/lib/site-packages/trio/_core/__init__.py +94 -0
  45. package/lib/site-packages/trio/_core/_asyncgens.py +243 -0
  46. package/lib/site-packages/trio/_core/_concat_tb.py +26 -0
  47. package/lib/site-packages/trio/_core/_entry_queue.py +223 -0
  48. package/lib/site-packages/trio/_core/_exceptions.py +169 -0
  49. package/lib/site-packages/trio/_core/_generated_instrumentation.py +50 -0
  50. package/lib/site-packages/trio/_core/_generated_io_epoll.py +98 -0
  51. package/lib/site-packages/trio/_core/_generated_io_kqueue.py +153 -0
  52. package/lib/site-packages/trio/_core/_generated_io_windows.py +204 -0
  53. package/lib/site-packages/trio/_core/_generated_run.py +269 -0
  54. package/lib/site-packages/trio/_core/_generated_windows_ffi.py +10 -0
  55. package/lib/site-packages/trio/_core/_instrumentation.py +117 -0
  56. package/lib/site-packages/trio/_core/_io_common.py +31 -0
  57. package/lib/site-packages/trio/_core/_io_epoll.py +385 -0
  58. package/lib/site-packages/trio/_core/_io_kqueue.py +292 -0
  59. package/lib/site-packages/trio/_core/_io_windows.py +1036 -0
  60. package/lib/site-packages/trio/_core/_ki.py +271 -0
  61. package/lib/site-packages/trio/_core/_local.py +104 -0
  62. package/lib/site-packages/trio/_core/_mock_clock.py +165 -0
  63. package/lib/site-packages/trio/_core/_parking_lot.py +317 -0
  64. package/lib/site-packages/trio/_core/_run.py +3148 -0
  65. package/lib/site-packages/trio/_core/_run_context.py +15 -0
  66. package/lib/site-packages/trio/_core/_tests/__init__.py +0 -0
  67. package/lib/site-packages/trio/_core/_tests/test_asyncgen.py +339 -0
  68. package/lib/site-packages/trio/_core/_tests/test_cancelled.py +222 -0
  69. package/lib/site-packages/trio/_core/_tests/test_exceptiongroup_gc.py +103 -0
  70. package/lib/site-packages/trio/_core/_tests/test_guest_mode.py +755 -0
  71. package/lib/site-packages/trio/_core/_tests/test_instrumentation.py +315 -0
  72. package/lib/site-packages/trio/_core/_tests/test_io.py +522 -0
  73. package/lib/site-packages/trio/_core/_tests/test_ki.py +703 -0
  74. package/lib/site-packages/trio/_core/_tests/test_local.py +118 -0
  75. package/lib/site-packages/trio/_core/_tests/test_mock_clock.py +193 -0
  76. package/lib/site-packages/trio/_core/_tests/test_parking_lot.py +389 -0
  77. package/lib/site-packages/trio/_core/_tests/test_run.py +3024 -0
  78. package/lib/site-packages/trio/_core/_tests/test_thread_cache.py +227 -0
  79. package/lib/site-packages/trio/_core/_tests/test_tutil.py +13 -0
  80. package/lib/site-packages/trio/_core/_tests/test_unbounded_queue.py +154 -0
  81. package/lib/site-packages/trio/_core/_tests/test_windows.py +305 -0
  82. package/lib/site-packages/trio/_core/_tests/tutil.py +117 -0
  83. package/lib/site-packages/trio/_core/_tests/type_tests/nursery_start.py +79 -0
  84. package/lib/site-packages/trio/_core/_tests/type_tests/run.py +51 -0
  85. package/lib/site-packages/trio/_core/_thread_cache.py +317 -0
  86. package/lib/site-packages/trio/_core/_traps.py +318 -0
  87. package/lib/site-packages/trio/_core/_unbounded_queue.py +163 -0
  88. package/lib/site-packages/trio/_core/_wakeup_socketpair.py +75 -0
  89. package/lib/site-packages/trio/_core/_windows_cffi.py +313 -0
  90. package/lib/site-packages/trio/_deprecate.py +171 -0
  91. package/lib/site-packages/trio/_dtls.py +1380 -0
  92. package/lib/site-packages/trio/_file_io.py +513 -0
  93. package/lib/site-packages/trio/_highlevel_generic.py +125 -0
  94. package/lib/site-packages/trio/_highlevel_open_tcp_listeners.py +251 -0
  95. package/lib/site-packages/trio/_highlevel_open_tcp_stream.py +397 -0
  96. package/lib/site-packages/trio/_highlevel_open_unix_stream.py +65 -0
  97. package/lib/site-packages/trio/_highlevel_serve_listeners.py +148 -0
  98. package/lib/site-packages/trio/_highlevel_socket.py +423 -0
  99. package/lib/site-packages/trio/_highlevel_ssl_helpers.py +180 -0
  100. package/lib/site-packages/trio/_path.py +289 -0
  101. package/lib/site-packages/trio/_repl.py +159 -0
  102. package/lib/site-packages/trio/_signals.py +185 -0
  103. package/lib/site-packages/trio/_socket.py +1326 -0
  104. package/lib/site-packages/trio/_ssl.py +964 -0
  105. package/lib/site-packages/trio/_subprocess.py +1178 -0
  106. package/lib/site-packages/trio/_subprocess_platform/__init__.py +123 -0
  107. package/lib/site-packages/trio/_subprocess_platform/kqueue.py +48 -0
  108. package/lib/site-packages/trio/_subprocess_platform/waitid.py +113 -0
  109. package/lib/site-packages/trio/_subprocess_platform/windows.py +11 -0
  110. package/lib/site-packages/trio/_sync.py +908 -0
  111. package/lib/site-packages/trio/_tests/__init__.py +0 -0
  112. package/lib/site-packages/trio/_tests/astrill-codesigning-cert.cer +0 -0
  113. package/lib/site-packages/trio/_tests/check_type_completeness.py +247 -0
  114. package/lib/site-packages/trio/_tests/module_with_deprecations.py +22 -0
  115. package/lib/site-packages/trio/_tests/pytest_plugin.py +54 -0
  116. package/lib/site-packages/trio/_tests/test_abc.py +72 -0
  117. package/lib/site-packages/trio/_tests/test_channel.py +750 -0
  118. package/lib/site-packages/trio/_tests/test_contextvars.py +56 -0
  119. package/lib/site-packages/trio/_tests/test_deprecate.py +277 -0
  120. package/lib/site-packages/trio/_tests/test_deprecate_strict_exception_groups_false.py +64 -0
  121. package/lib/site-packages/trio/_tests/test_dtls.py +950 -0
  122. package/lib/site-packages/trio/_tests/test_exports.py +626 -0
  123. package/lib/site-packages/trio/_tests/test_fakenet.py +317 -0
  124. package/lib/site-packages/trio/_tests/test_file_io.py +269 -0
  125. package/lib/site-packages/trio/_tests/test_highlevel_generic.py +98 -0
  126. package/lib/site-packages/trio/_tests/test_highlevel_open_tcp_listeners.py +419 -0
  127. package/lib/site-packages/trio/_tests/test_highlevel_open_tcp_stream.py +693 -0
  128. package/lib/site-packages/trio/_tests/test_highlevel_open_unix_stream.py +86 -0
  129. package/lib/site-packages/trio/_tests/test_highlevel_serve_listeners.py +186 -0
  130. package/lib/site-packages/trio/_tests/test_highlevel_socket.py +336 -0
  131. package/lib/site-packages/trio/_tests/test_highlevel_ssl_helpers.py +169 -0
  132. package/lib/site-packages/trio/_tests/test_path.py +279 -0
  133. package/lib/site-packages/trio/_tests/test_repl.py +428 -0
  134. package/lib/site-packages/trio/_tests/test_scheduler_determinism.py +47 -0
  135. package/lib/site-packages/trio/_tests/test_signals.py +186 -0
  136. package/lib/site-packages/trio/_tests/test_socket.py +1253 -0
  137. package/lib/site-packages/trio/_tests/test_ssl.py +1371 -0
  138. package/lib/site-packages/trio/_tests/test_subprocess.py +767 -0
  139. package/lib/site-packages/trio/_tests/test_sync.py +735 -0
  140. package/lib/site-packages/trio/_tests/test_testing.py +682 -0
  141. package/lib/site-packages/trio/_tests/test_testing_raisesgroup.py +1128 -0
  142. package/lib/site-packages/trio/_tests/test_threads.py +1173 -0
  143. package/lib/site-packages/trio/_tests/test_timeouts.py +281 -0
  144. package/lib/site-packages/trio/_tests/test_tracing.py +88 -0
  145. package/lib/site-packages/trio/_tests/test_trio.py +8 -0
  146. package/lib/site-packages/trio/_tests/test_unix_pipes.py +288 -0
  147. package/lib/site-packages/trio/_tests/test_util.py +349 -0
  148. package/lib/site-packages/trio/_tests/test_wait_for_object.py +225 -0
  149. package/lib/site-packages/trio/_tests/test_windows_pipes.py +112 -0
  150. package/lib/site-packages/trio/_tests/tools/__init__.py +0 -0
  151. package/lib/site-packages/trio/_tests/tools/test_gen_exports.py +179 -0
  152. package/lib/site-packages/trio/_tests/tools/test_mypy_annotate.py +140 -0
  153. package/lib/site-packages/trio/_tests/tools/test_sync_requirements.py +80 -0
  154. package/lib/site-packages/trio/_tests/type_tests/check_wraps.py +9 -0
  155. package/lib/site-packages/trio/_tests/type_tests/open_memory_channel.py +4 -0
  156. package/lib/site-packages/trio/_tests/type_tests/path.py +140 -0
  157. package/lib/site-packages/trio/_tests/type_tests/subprocesses.py +23 -0
  158. package/lib/site-packages/trio/_tests/type_tests/task_status.py +29 -0
  159. package/lib/site-packages/trio/_threads.py +610 -0
  160. package/lib/site-packages/trio/_timeouts.py +197 -0
  161. package/lib/site-packages/trio/_tools/__init__.py +0 -0
  162. package/lib/site-packages/trio/_tools/gen_exports.py +401 -0
  163. package/lib/site-packages/trio/_tools/mypy_annotate.py +126 -0
  164. package/lib/site-packages/trio/_tools/sync_requirements.py +98 -0
  165. package/lib/site-packages/trio/_tools/windows_ffi_build.py +220 -0
  166. package/lib/site-packages/trio/_unix_pipes.py +197 -0
  167. package/lib/site-packages/trio/_util.py +385 -0
  168. package/lib/site-packages/trio/_version.py +3 -0
  169. package/lib/site-packages/trio/_wait_for_object.py +67 -0
  170. package/lib/site-packages/trio/_windows_pipes.py +144 -0
  171. package/lib/site-packages/trio/abc.py +23 -0
  172. package/lib/site-packages/trio/from_thread.py +13 -0
  173. package/lib/site-packages/trio/lowlevel.py +95 -0
  174. package/lib/site-packages/trio/py.typed +0 -0
  175. package/lib/site-packages/trio/socket.py +602 -0
  176. package/lib/site-packages/trio/testing/__init__.py +58 -0
  177. package/lib/site-packages/trio/testing/_check_streams.py +570 -0
  178. package/lib/site-packages/trio/testing/_checkpoints.py +69 -0
  179. package/lib/site-packages/trio/testing/_fake_net.py +584 -0
  180. package/lib/site-packages/trio/testing/_memory_streams.py +633 -0
  181. package/lib/site-packages/trio/testing/_network.py +36 -0
  182. package/lib/site-packages/trio/testing/_raises_group.py +1015 -0
  183. package/lib/site-packages/trio/testing/_sequencer.py +87 -0
  184. package/lib/site-packages/trio/testing/_trio_test.py +50 -0
  185. package/lib/site-packages/trio/to_thread.py +4 -0
  186. package/lib/site-packages/trio-0.33.0.dist-info/INSTALLER +1 -0
  187. package/lib/site-packages/trio-0.33.0.dist-info/METADATA +186 -0
  188. package/lib/site-packages/trio-0.33.0.dist-info/RECORD +156 -0
  189. package/lib/site-packages/trio-0.33.0.dist-info/REQUESTED +0 -0
  190. package/lib/site-packages/trio-0.33.0.dist-info/WHEEL +5 -0
  191. package/lib/site-packages/trio-0.33.0.dist-info/entry_points.txt +2 -0
  192. package/lib/site-packages/trio-0.33.0.dist-info/licenses/LICENSE +3 -0
  193. package/lib/site-packages/trio-0.33.0.dist-info/licenses/LICENSE.APACHE2 +202 -0
  194. package/lib/site-packages/trio-0.33.0.dist-info/licenses/LICENSE.MIT +22 -0
  195. package/lib/site-packages/trio-0.33.0.dist-info/top_level.txt +1 -0
  196. package/package.json +1 -1
@@ -0,0 +1,1173 @@
1
+ from __future__ import annotations
2
+
3
+ import contextvars
4
+ import queue as stdlib_queue
5
+ import re
6
+ import sys
7
+ import threading
8
+ import time
9
+ import weakref
10
+ from functools import partial
11
+ from typing import (
12
+ TYPE_CHECKING,
13
+ NoReturn,
14
+ TypeVar,
15
+ )
16
+
17
+ import pytest
18
+ import sniffio
19
+
20
+ from .. import (
21
+ CancelScope,
22
+ CapacityLimiter,
23
+ Event,
24
+ _core,
25
+ fail_after,
26
+ move_on_after,
27
+ sleep,
28
+ sleep_forever,
29
+ )
30
+ from .._core._tests.test_ki import ki_self
31
+ from .._core._tests.tutil import slow
32
+ from .._threads import (
33
+ active_thread_count,
34
+ current_default_thread_limiter,
35
+ from_thread_check_cancelled,
36
+ from_thread_run,
37
+ from_thread_run_sync,
38
+ to_thread_run_sync,
39
+ wait_all_threads_completed,
40
+ )
41
+ from ..testing import wait_all_tasks_blocked
42
+
43
+ if TYPE_CHECKING:
44
+ from collections.abc import AsyncGenerator, Awaitable, Callable
45
+
46
+ from outcome import Outcome
47
+
48
+ from ..lowlevel import Task
49
+
50
+ RecordType = list[tuple[str, threading.Thread | type[BaseException]]]
51
+ T = TypeVar("T")
52
+
53
+
54
+ async def test_do_in_trio_thread() -> None:
55
+ trio_thread = threading.current_thread()
56
+
57
+ async def check_case( # type: ignore[explicit-any]
58
+ do_in_trio_thread: Callable[..., threading.Thread],
59
+ fn: Callable[..., T | Awaitable[T]],
60
+ expected: tuple[str, T],
61
+ trio_token: _core.TrioToken | None = None,
62
+ ) -> None:
63
+ record: RecordType = []
64
+
65
+ def threadfn() -> None:
66
+ try:
67
+ record.append(("start", threading.current_thread()))
68
+ x = do_in_trio_thread(fn, record, trio_token=trio_token)
69
+ record.append(("got", x))
70
+ except BaseException as exc:
71
+ print(exc)
72
+ record.append(("error", type(exc)))
73
+
74
+ child_thread = threading.Thread(target=threadfn, daemon=True)
75
+ child_thread.start()
76
+ while child_thread.is_alive():
77
+ print("yawn")
78
+ await sleep(0.01)
79
+ assert record == [("start", child_thread), ("f", trio_thread), expected]
80
+
81
+ token = _core.current_trio_token()
82
+
83
+ def f1(record: RecordType) -> int:
84
+ assert not _core.currently_ki_protected()
85
+ record.append(("f", threading.current_thread()))
86
+ return 2
87
+
88
+ await check_case(from_thread_run_sync, f1, ("got", 2), trio_token=token)
89
+
90
+ def f2(record: RecordType) -> NoReturn:
91
+ assert not _core.currently_ki_protected()
92
+ record.append(("f", threading.current_thread()))
93
+ raise ValueError
94
+
95
+ await check_case(from_thread_run_sync, f2, ("error", ValueError), trio_token=token)
96
+
97
+ async def f3(record: RecordType) -> int:
98
+ assert not _core.currently_ki_protected()
99
+ await _core.checkpoint()
100
+ record.append(("f", threading.current_thread()))
101
+ return 3
102
+
103
+ await check_case(from_thread_run, f3, ("got", 3), trio_token=token)
104
+
105
+ async def f4(record: RecordType) -> NoReturn:
106
+ assert not _core.currently_ki_protected()
107
+ await _core.checkpoint()
108
+ record.append(("f", threading.current_thread()))
109
+ raise KeyError
110
+
111
+ await check_case(from_thread_run, f4, ("error", KeyError), trio_token=token)
112
+
113
+
114
+ async def test_do_in_trio_thread_from_trio_thread() -> None:
115
+ with pytest.raises(RuntimeError):
116
+ from_thread_run_sync(lambda: None) # pragma: no branch
117
+
118
+ async def foo() -> None: # pragma: no cover
119
+ pass
120
+
121
+ with pytest.raises(RuntimeError):
122
+ from_thread_run(foo)
123
+
124
+
125
+ def test_run_in_trio_thread_ki() -> None:
126
+ # if we get a control-C during a run_in_trio_thread, then it propagates
127
+ # back to the caller (slick!)
128
+ record = set()
129
+
130
+ async def check_run_in_trio_thread() -> None:
131
+ token = _core.current_trio_token()
132
+
133
+ def trio_thread_fn() -> None:
134
+ print("in Trio thread")
135
+ assert not _core.currently_ki_protected()
136
+ print("ki_self")
137
+ try:
138
+ ki_self()
139
+ finally:
140
+ import sys
141
+
142
+ print("finally", sys.exc_info())
143
+
144
+ async def trio_thread_afn() -> None:
145
+ trio_thread_fn()
146
+
147
+ def external_thread_fn() -> None:
148
+ try:
149
+ print("running")
150
+ from_thread_run_sync(trio_thread_fn, trio_token=token)
151
+ except KeyboardInterrupt:
152
+ print("ok1")
153
+ record.add("ok1")
154
+ try:
155
+ from_thread_run(trio_thread_afn, trio_token=token)
156
+ except KeyboardInterrupt:
157
+ print("ok2")
158
+ record.add("ok2")
159
+
160
+ thread = threading.Thread(target=external_thread_fn)
161
+ thread.start()
162
+ print("waiting")
163
+ while thread.is_alive(): # noqa: ASYNC110
164
+ await sleep(0.01) # Fine to poll in tests.
165
+ print("waited, joining")
166
+ thread.join()
167
+ print("done")
168
+
169
+ _core.run(check_run_in_trio_thread)
170
+ assert record == {"ok1", "ok2"}
171
+
172
+
173
+ def test_await_in_trio_thread_while_main_exits() -> None:
174
+ record = []
175
+ ev = Event()
176
+
177
+ async def trio_fn() -> None:
178
+ record.append("sleeping")
179
+ ev.set()
180
+ await _core.wait_task_rescheduled(lambda _: _core.Abort.SUCCEEDED)
181
+
182
+ def thread_fn(token: _core.TrioToken) -> None:
183
+ try:
184
+ from_thread_run(trio_fn, trio_token=token)
185
+ except _core.Cancelled:
186
+ record.append("cancelled")
187
+
188
+ async def main() -> threading.Thread:
189
+ token = _core.current_trio_token()
190
+ thread = threading.Thread(target=thread_fn, args=(token,))
191
+ thread.start()
192
+ await ev.wait()
193
+ assert record == ["sleeping"]
194
+ return thread
195
+
196
+ thread = _core.run(main)
197
+ thread.join()
198
+ assert record == ["sleeping", "cancelled"]
199
+
200
+
201
+ async def test_named_thread() -> None:
202
+ ending = " from trio._tests.test_threads.test_named_thread"
203
+
204
+ def inner(name: str = "inner" + ending) -> threading.Thread:
205
+ assert threading.current_thread().name == name
206
+ return threading.current_thread()
207
+
208
+ def f(name: str) -> Callable[[], threading.Thread]:
209
+ return partial(inner, name)
210
+
211
+ # test defaults
212
+ await to_thread_run_sync(inner)
213
+ await to_thread_run_sync(inner, thread_name=None)
214
+
215
+ # functools.partial doesn't have __name__, so defaults to None
216
+ await to_thread_run_sync(f("None" + ending))
217
+
218
+ # test that you can set a custom name, and that it's reset afterwards
219
+ async def test_thread_name(name: str) -> None:
220
+ thread = await to_thread_run_sync(f(name), thread_name=name)
221
+ assert re.match(r"Trio thread [0-9]*", thread.name)
222
+
223
+ await test_thread_name("")
224
+ await test_thread_name("fobiedoo")
225
+ await test_thread_name("name_longer_than_15_characters")
226
+
227
+ await test_thread_name("💙")
228
+
229
+
230
+ def _get_thread_name(ident: int | None = None) -> str | None:
231
+ import ctypes
232
+ import ctypes.util
233
+
234
+ libpthread_path = ctypes.util.find_library("pthread")
235
+ if not libpthread_path:
236
+ # musl includes pthread functions directly in libc.so
237
+ # (but note that find_library("c") does not work on musl,
238
+ # see: https://github.com/python/cpython/issues/65821)
239
+ # so try that library instead
240
+ # if it doesn't exist, CDLL() will fail below
241
+ libpthread_path = "libc.so"
242
+ try:
243
+ libpthread = ctypes.CDLL(libpthread_path)
244
+ except Exception:
245
+ print(f"no pthread on {sys.platform}")
246
+ return None
247
+
248
+ pthread_getname_np = getattr(libpthread, "pthread_getname_np", None)
249
+
250
+ # this should never fail on any platforms afaik
251
+ assert pthread_getname_np
252
+
253
+ # thankfully getname signature doesn't differ between platforms
254
+ pthread_getname_np.argtypes = [
255
+ ctypes.c_void_p,
256
+ ctypes.c_char_p,
257
+ ctypes.c_size_t,
258
+ ]
259
+ pthread_getname_np.restype = ctypes.c_int
260
+
261
+ name_buffer = ctypes.create_string_buffer(b"", size=16)
262
+ if ident is None:
263
+ ident = threading.get_ident()
264
+ assert pthread_getname_np(ident, name_buffer, 16) == 0
265
+ try:
266
+ return name_buffer.value.decode()
267
+ except UnicodeDecodeError as e: # pragma: no cover
268
+ # used for debugging when testing via CI
269
+ pytest.fail(f"value: {name_buffer.value!r}, exception: {e}")
270
+
271
+
272
+ # test os thread naming
273
+ # this depends on pthread being available, which is the case on 99.9% of linux machines
274
+ # and most mac machines. So unless the platform is linux it will just skip
275
+ # in case it fails to fetch the os thread name.
276
+ async def test_named_thread_os() -> None:
277
+ def inner(name: str) -> threading.Thread:
278
+ os_thread_name = _get_thread_name()
279
+ if os_thread_name is None and sys.platform != "linux":
280
+ pytest.skip(f"no pthread OS support on {sys.platform}")
281
+ else:
282
+ assert os_thread_name == name[:15]
283
+
284
+ return threading.current_thread()
285
+
286
+ def f(name: str) -> Callable[[], threading.Thread]:
287
+ return partial(inner, name)
288
+
289
+ # test defaults
290
+ default = "None from trio._tests.test_threads.test_named_thread"
291
+ await to_thread_run_sync(f(default))
292
+ await to_thread_run_sync(f(default), thread_name=None)
293
+
294
+ # test that you can set a custom name, and that it's reset afterwards
295
+ async def test_thread_name(name: str, expected: str | None = None) -> None:
296
+ if expected is None:
297
+ expected = name
298
+ thread = await to_thread_run_sync(f(expected), thread_name=name)
299
+
300
+ os_thread_name = _get_thread_name(thread.ident)
301
+ assert os_thread_name is not None, "should skip earlier if this is the case"
302
+ assert re.match(r"Trio thread [0-9]*", os_thread_name)
303
+
304
+ await test_thread_name("")
305
+ await test_thread_name("fobiedoo")
306
+ await test_thread_name("name_longer_than_15_characters")
307
+
308
+ await test_thread_name("💙", expected="?")
309
+
310
+
311
+ def test_has_pthread_setname_np() -> None:
312
+ from trio._core._thread_cache import get_os_thread_name_func
313
+
314
+ k = get_os_thread_name_func()
315
+ if k is None:
316
+ assert sys.platform != "linux"
317
+ pytest.skip(f"no pthread_setname_np on {sys.platform}")
318
+
319
+
320
+ async def test_run_in_worker_thread() -> None:
321
+ trio_thread = threading.current_thread()
322
+
323
+ def f(x: T) -> tuple[T, threading.Thread]:
324
+ return (x, threading.current_thread())
325
+
326
+ x, child_thread = await to_thread_run_sync(f, 1)
327
+ assert x == 1
328
+ assert child_thread != trio_thread
329
+
330
+ def g() -> NoReturn:
331
+ raise ValueError(threading.current_thread())
332
+
333
+ with pytest.raises(
334
+ ValueError,
335
+ match=r"^<Thread\(Trio thread \d+, started daemon \d+\)>$",
336
+ ) as excinfo:
337
+ await to_thread_run_sync(g)
338
+ print(excinfo.value.args)
339
+ assert excinfo.value.args[0] != trio_thread
340
+
341
+
342
+ async def test_run_in_worker_thread_cancellation() -> None:
343
+ register: list[str | None] = [None]
344
+
345
+ def f(q: stdlib_queue.Queue[None]) -> None:
346
+ # Make the thread block for a controlled amount of time
347
+ register[0] = "blocking"
348
+ q.get()
349
+ register[0] = "finished"
350
+
351
+ async def child(q: stdlib_queue.Queue[None], abandon_on_cancel: bool) -> None:
352
+ record.append("start")
353
+ try:
354
+ return await to_thread_run_sync(f, q, abandon_on_cancel=abandon_on_cancel)
355
+ finally:
356
+ record.append("exit")
357
+
358
+ record: list[str] = []
359
+ q: stdlib_queue.Queue[None] = stdlib_queue.Queue()
360
+ async with _core.open_nursery() as nursery:
361
+ nursery.start_soon(child, q, True)
362
+ # Give it a chance to get started. (This is important because
363
+ # to_thread_run_sync does a checkpoint_if_cancelled before
364
+ # blocking on the thread, and we don't want to trigger this.)
365
+ await wait_all_tasks_blocked()
366
+ assert record == ["start"]
367
+ # Then cancel it.
368
+ nursery.cancel_scope.cancel()
369
+ # The task exited, but the thread didn't:
370
+ assert register[0] != "finished"
371
+ # Put the thread out of its misery:
372
+ q.put(None)
373
+ while register[0] != "finished":
374
+ time.sleep(0.01) # noqa: ASYNC251 # Need to wait for OS thread
375
+
376
+ # This one can't be cancelled
377
+ record = []
378
+ register[0] = None
379
+ async with _core.open_nursery() as nursery:
380
+ nursery.start_soon(child, q, False)
381
+ await wait_all_tasks_blocked()
382
+ nursery.cancel_scope.cancel()
383
+ with _core.CancelScope(shield=True):
384
+ for _ in range(10):
385
+ await _core.checkpoint()
386
+ # It's still running
387
+ assert record == ["start"]
388
+ q.put(None)
389
+ # Now it exits
390
+
391
+ # But if we cancel *before* it enters, the entry is itself a cancellation
392
+ # point
393
+ with _core.CancelScope() as scope:
394
+ scope.cancel()
395
+ await child(q, False)
396
+ assert scope.cancelled_caught
397
+
398
+
399
+ # Make sure that if trio.run exits, and then the thread finishes, then that's
400
+ # handled gracefully. (Requires that the thread result machinery be prepared
401
+ # for call_soon to raise RunFinishedError.)
402
+ def test_run_in_worker_thread_abandoned(
403
+ capfd: pytest.CaptureFixture[str],
404
+ monkeypatch: pytest.MonkeyPatch,
405
+ ) -> None:
406
+ monkeypatch.setattr(_core._thread_cache, "IDLE_TIMEOUT", 0.01)
407
+
408
+ q1: stdlib_queue.Queue[None] = stdlib_queue.Queue()
409
+ q2: stdlib_queue.Queue[threading.Thread] = stdlib_queue.Queue()
410
+
411
+ def thread_fn() -> None:
412
+ q1.get()
413
+ q2.put(threading.current_thread())
414
+
415
+ async def main() -> None:
416
+ async def child() -> None:
417
+ await to_thread_run_sync(thread_fn, abandon_on_cancel=True)
418
+
419
+ async with _core.open_nursery() as nursery:
420
+ nursery.start_soon(child)
421
+ await wait_all_tasks_blocked()
422
+ nursery.cancel_scope.cancel()
423
+
424
+ _core.run(main)
425
+
426
+ q1.put(None)
427
+ # This makes sure:
428
+ # - the thread actually ran
429
+ # - that thread has finished before we check for its output
430
+ thread = q2.get()
431
+ while thread.is_alive():
432
+ time.sleep(0.01) # pragma: no cover
433
+
434
+ # Make sure we don't have a "Exception in thread ..." dump to the console:
435
+ out, err = capfd.readouterr()
436
+ assert "Exception in thread" not in out
437
+ assert "Exception in thread" not in err
438
+
439
+
440
+ @pytest.mark.parametrize("MAX", [3, 5, 10])
441
+ @pytest.mark.parametrize("cancel", [False, True])
442
+ @pytest.mark.parametrize("use_default_limiter", [False, True])
443
+ async def test_run_in_worker_thread_limiter(
444
+ MAX: int,
445
+ cancel: bool,
446
+ use_default_limiter: bool,
447
+ ) -> None:
448
+ # This test is a bit tricky. The goal is to make sure that if we set
449
+ # limiter=CapacityLimiter(MAX), then in fact only MAX threads are ever
450
+ # running at a time, even if there are more concurrent calls to
451
+ # to_thread_run_sync, and even if some of those are cancelled. And
452
+ # also to make sure that the default limiter actually limits.
453
+ COUNT = 2 * MAX
454
+ gate = threading.Event()
455
+ lock = threading.Lock()
456
+ if use_default_limiter:
457
+ c = current_default_thread_limiter()
458
+ orig_total_tokens = c.total_tokens
459
+ c.total_tokens = MAX
460
+ limiter_arg = None
461
+ else:
462
+ c = CapacityLimiter(MAX)
463
+ orig_total_tokens = MAX
464
+ limiter_arg = c
465
+ try:
466
+ # We used to use regular variables and 'nonlocal' here, but it turns
467
+ # out that it's not safe to assign to closed-over variables that are
468
+ # visible in multiple threads, at least as of CPython 3.10 and PyPy
469
+ # 7.3:
470
+ #
471
+ # https://bugs.python.org/issue30744
472
+ # https://bitbucket.org/pypy/pypy/issues/2591/
473
+ #
474
+ # Mutating them in-place is OK though (as long as you use proper
475
+ # locking etc.).
476
+ class state:
477
+ ran: int
478
+ high_water: int
479
+ running: int
480
+ parked: int
481
+
482
+ state.ran = 0
483
+ state.high_water = 0
484
+ state.running = 0
485
+ state.parked = 0
486
+
487
+ token = _core.current_trio_token()
488
+
489
+ def thread_fn(cancel_scope: CancelScope) -> None:
490
+ print("thread_fn start")
491
+ from_thread_run_sync(cancel_scope.cancel, trio_token=token)
492
+ with lock:
493
+ state.ran += 1
494
+ state.running += 1
495
+ state.high_water = max(state.high_water, state.running)
496
+ # The Trio thread below watches this value and uses it as a
497
+ # signal that all the stats calculations have finished.
498
+ state.parked += 1
499
+ gate.wait()
500
+ with lock:
501
+ state.parked -= 1
502
+ state.running -= 1
503
+ print("thread_fn exiting")
504
+
505
+ async def run_thread(event: Event) -> None:
506
+ with _core.CancelScope() as cancel_scope:
507
+ await to_thread_run_sync(
508
+ thread_fn,
509
+ cancel_scope,
510
+ abandon_on_cancel=cancel,
511
+ limiter=limiter_arg,
512
+ )
513
+ print("run_thread finished, cancelled:", cancel_scope.cancelled_caught)
514
+ event.set()
515
+
516
+ async with _core.open_nursery() as nursery:
517
+ print("spawning")
518
+ events = []
519
+ for _ in range(COUNT):
520
+ events.append(Event())
521
+ nursery.start_soon(run_thread, events[-1])
522
+ await wait_all_tasks_blocked()
523
+ # In the cancel case, we in particular want to make sure that the
524
+ # cancelled tasks don't release the semaphore. So let's wait until
525
+ # at least one of them has exited, and that everything has had a
526
+ # chance to settle down from this, before we check that everyone
527
+ # who's supposed to be waiting is waiting:
528
+ if cancel:
529
+ print("waiting for first cancellation to clear")
530
+ await events[0].wait()
531
+ await wait_all_tasks_blocked()
532
+ # Then wait until the first MAX threads are parked in gate.wait(),
533
+ # and the next MAX threads are parked on the semaphore, to make
534
+ # sure no-one is sneaking past, and to make sure the high_water
535
+ # check below won't fail due to scheduling issues. (It could still
536
+ # fail if too many threads are let through here.)
537
+ while ( # noqa: ASYNC110
538
+ state.parked != MAX or c.statistics().tasks_waiting != MAX
539
+ ):
540
+ await sleep(0.01) # pragma: no cover
541
+ # Then release the threads
542
+ gate.set()
543
+
544
+ assert state.high_water == MAX
545
+
546
+ if cancel:
547
+ # Some threads might still be running; need to wait to them to
548
+ # finish before checking that all threads ran. We can do this
549
+ # using the CapacityLimiter.
550
+ while c.borrowed_tokens > 0: # noqa: ASYNC110
551
+ await sleep(0.01) # pragma: no cover
552
+
553
+ assert state.ran == COUNT
554
+ assert state.running == 0
555
+ finally:
556
+ c.total_tokens = orig_total_tokens
557
+
558
+
559
+ async def test_run_in_worker_thread_custom_limiter() -> None:
560
+ # Basically just checking that we only call acquire_on_behalf_of and
561
+ # release_on_behalf_of, since that's part of our documented API.
562
+ record = []
563
+
564
+ class CustomLimiter:
565
+ async def acquire_on_behalf_of(self, borrower: Task) -> None:
566
+ record.append("acquire")
567
+ self._borrower = borrower
568
+
569
+ def release_on_behalf_of(self, borrower: Task) -> None:
570
+ record.append("release")
571
+ assert borrower == self._borrower
572
+
573
+ # TODO: should CapacityLimiter have an abc or protocol so users can modify it?
574
+ # because currently it's `final` so writing code like this is not allowed.
575
+ await to_thread_run_sync(lambda: None, limiter=CustomLimiter()) # type: ignore[arg-type]
576
+ assert record == ["acquire", "release"]
577
+
578
+
579
+ async def test_run_in_worker_thread_limiter_error() -> None:
580
+ record = []
581
+
582
+ class BadCapacityLimiter:
583
+ async def acquire_on_behalf_of(self, borrower: Task) -> None:
584
+ record.append("acquire")
585
+
586
+ def release_on_behalf_of(self, borrower: Task) -> NoReturn:
587
+ record.append("release")
588
+ raise ValueError("release on behalf")
589
+
590
+ bs = BadCapacityLimiter()
591
+
592
+ with pytest.raises(ValueError, match=r"^release on behalf$") as excinfo:
593
+ await to_thread_run_sync(lambda: None, limiter=bs) # type: ignore[arg-type]
594
+ assert excinfo.value.__context__ is None
595
+ assert record == ["acquire", "release"]
596
+ record = []
597
+
598
+ # If the original function raised an error, then the semaphore error
599
+ # chains with it
600
+ d: dict[str, object] = {}
601
+ with pytest.raises(ValueError, match=r"^release on behalf$") as excinfo:
602
+ await to_thread_run_sync(lambda: d["x"], limiter=bs) # type: ignore[arg-type]
603
+ assert isinstance(excinfo.value.__context__, KeyError)
604
+ assert record == ["acquire", "release"]
605
+
606
+
607
+ async def test_run_in_worker_thread_fail_to_spawn(
608
+ monkeypatch: pytest.MonkeyPatch,
609
+ ) -> None:
610
+ # Test the unlikely but possible case where trying to spawn a thread fails
611
+ def bad_start(self: object, *args: object) -> NoReturn:
612
+ raise RuntimeError("the engines canna take it captain")
613
+
614
+ monkeypatch.setattr(_core._thread_cache.ThreadCache, "start_thread_soon", bad_start)
615
+
616
+ limiter = current_default_thread_limiter()
617
+ assert limiter.borrowed_tokens == 0
618
+
619
+ # We get an appropriate error, and the limiter is cleanly released
620
+ with pytest.raises(RuntimeError) as excinfo:
621
+ await to_thread_run_sync(lambda: None) # pragma: no cover
622
+ assert "engines" in str(excinfo.value)
623
+
624
+ assert limiter.borrowed_tokens == 0
625
+
626
+
627
+ async def test_trio_to_thread_run_sync_token() -> None:
628
+ # Test that to_thread_run_sync automatically injects the current trio token
629
+ # into a spawned thread
630
+ def thread_fn() -> _core.TrioToken:
631
+ callee_token = from_thread_run_sync(_core.current_trio_token)
632
+ return callee_token
633
+
634
+ caller_token = _core.current_trio_token()
635
+ callee_token = await to_thread_run_sync(thread_fn)
636
+ assert callee_token == caller_token
637
+
638
+
639
+ async def test_trio_to_thread_run_sync_expected_error() -> None:
640
+ # Test correct error when passed async function
641
+ async def async_fn() -> None: # pragma: no cover
642
+ pass
643
+
644
+ with pytest.raises(TypeError, match="expected a sync function"):
645
+ await to_thread_run_sync(async_fn) # type: ignore[unused-coroutine]
646
+
647
+
648
+ trio_test_contextvar: contextvars.ContextVar[str] = contextvars.ContextVar(
649
+ "trio_test_contextvar",
650
+ )
651
+
652
+
653
+ async def test_trio_to_thread_run_sync_contextvars() -> None:
654
+ trio_thread = threading.current_thread()
655
+ trio_test_contextvar.set("main")
656
+
657
+ def f() -> tuple[str, threading.Thread]:
658
+ value = trio_test_contextvar.get()
659
+ with pytest.raises(sniffio.AsyncLibraryNotFoundError):
660
+ sniffio.current_async_library()
661
+ return (value, threading.current_thread())
662
+
663
+ value, child_thread = await to_thread_run_sync(f)
664
+ assert value == "main"
665
+ assert child_thread != trio_thread
666
+
667
+ def g() -> tuple[str, str, threading.Thread]:
668
+ parent_value = trio_test_contextvar.get()
669
+ trio_test_contextvar.set("worker")
670
+ inner_value = trio_test_contextvar.get()
671
+ with pytest.raises(sniffio.AsyncLibraryNotFoundError):
672
+ sniffio.current_async_library()
673
+ return (
674
+ parent_value,
675
+ inner_value,
676
+ threading.current_thread(),
677
+ )
678
+
679
+ parent_value, inner_value, child_thread = await to_thread_run_sync(g)
680
+ current_value = trio_test_contextvar.get()
681
+ assert parent_value == "main"
682
+ assert inner_value == "worker"
683
+ assert current_value == "main", (
684
+ "The contextvar value set on the worker would not propagate back to the main"
685
+ " thread"
686
+ )
687
+ assert sniffio.current_async_library() == "trio"
688
+
689
+
690
+ async def test_trio_from_thread_run_sync() -> None:
691
+ # Test that to_thread_run_sync correctly "hands off" the trio token to
692
+ # trio.from_thread.run_sync()
693
+ def thread_fn_1() -> float:
694
+ trio_time = from_thread_run_sync(_core.current_time)
695
+ return trio_time
696
+
697
+ trio_time = await to_thread_run_sync(thread_fn_1)
698
+ assert isinstance(trio_time, float)
699
+
700
+ # Test correct error when passed async function
701
+ async def async_fn() -> None: # pragma: no cover
702
+ pass
703
+
704
+ def thread_fn_2() -> None:
705
+ from_thread_run_sync(async_fn) # type: ignore[unused-coroutine]
706
+
707
+ with pytest.raises(TypeError, match="expected a synchronous function"):
708
+ await to_thread_run_sync(thread_fn_2)
709
+
710
+
711
+ async def test_trio_from_thread_run() -> None:
712
+ # Test that to_thread_run_sync correctly "hands off" the trio token to
713
+ # trio.from_thread.run()
714
+ record = []
715
+
716
+ async def back_in_trio_fn() -> None:
717
+ _core.current_time() # implicitly checks that we're in trio
718
+ record.append("back in trio")
719
+
720
+ def thread_fn() -> None:
721
+ record.append("in thread")
722
+ from_thread_run(back_in_trio_fn)
723
+
724
+ await to_thread_run_sync(thread_fn)
725
+ assert record == ["in thread", "back in trio"]
726
+
727
+ # Test correct error when passed sync function
728
+ def sync_fn() -> None: # pragma: no cover
729
+ pass
730
+
731
+ with pytest.raises(TypeError, match="appears to be synchronous"):
732
+ await to_thread_run_sync(from_thread_run, sync_fn) # type: ignore[arg-type]
733
+
734
+
735
+ async def test_trio_from_thread_token() -> None:
736
+ # Test that to_thread_run_sync and spawned trio.from_thread.run_sync()
737
+ # share the same Trio token
738
+ def thread_fn() -> _core.TrioToken:
739
+ callee_token = from_thread_run_sync(_core.current_trio_token)
740
+ return callee_token
741
+
742
+ caller_token = _core.current_trio_token()
743
+ callee_token = await to_thread_run_sync(thread_fn)
744
+ assert callee_token == caller_token
745
+
746
+
747
+ async def test_trio_from_thread_token_kwarg() -> None:
748
+ # Test that to_thread_run_sync and spawned trio.from_thread.run_sync() can
749
+ # use an explicitly defined token
750
+ def thread_fn(token: _core.TrioToken) -> _core.TrioToken:
751
+ callee_token = from_thread_run_sync(_core.current_trio_token, trio_token=token)
752
+ return callee_token
753
+
754
+ caller_token = _core.current_trio_token()
755
+ callee_token = await to_thread_run_sync(thread_fn, caller_token)
756
+ assert callee_token == caller_token
757
+
758
+
759
+ async def test_from_thread_no_token() -> None:
760
+ # Test that a "raw call" to trio.from_thread.run() fails because no token
761
+ # has been provided
762
+
763
+ with pytest.raises(RuntimeError):
764
+ from_thread_run_sync(_core.current_time)
765
+
766
+
767
+ async def test_trio_from_thread_run_sync_contextvars() -> None:
768
+ trio_test_contextvar.set("main")
769
+
770
+ def thread_fn() -> tuple[str, str, str, str, str]:
771
+ thread_parent_value = trio_test_contextvar.get()
772
+ trio_test_contextvar.set("worker")
773
+ thread_current_value = trio_test_contextvar.get()
774
+ with pytest.raises(sniffio.AsyncLibraryNotFoundError):
775
+ sniffio.current_async_library()
776
+
777
+ def back_in_main() -> tuple[str, str]:
778
+ back_parent_value = trio_test_contextvar.get()
779
+ trio_test_contextvar.set("back_in_main")
780
+ back_current_value = trio_test_contextvar.get()
781
+ assert sniffio.current_async_library() == "trio"
782
+ return back_parent_value, back_current_value
783
+
784
+ back_parent_value, back_current_value = from_thread_run_sync(back_in_main)
785
+ thread_after_value = trio_test_contextvar.get()
786
+ with pytest.raises(sniffio.AsyncLibraryNotFoundError):
787
+ sniffio.current_async_library()
788
+ return (
789
+ thread_parent_value,
790
+ thread_current_value,
791
+ thread_after_value,
792
+ back_parent_value,
793
+ back_current_value,
794
+ )
795
+
796
+ (
797
+ thread_parent_value,
798
+ thread_current_value,
799
+ thread_after_value,
800
+ back_parent_value,
801
+ back_current_value,
802
+ ) = await to_thread_run_sync(thread_fn)
803
+ current_value = trio_test_contextvar.get()
804
+ assert current_value == thread_parent_value == "main"
805
+ assert thread_current_value == back_parent_value == thread_after_value == "worker"
806
+ assert sniffio.current_async_library() == "trio"
807
+ assert back_current_value == "back_in_main"
808
+
809
+
810
+ async def test_trio_from_thread_run_contextvars() -> None:
811
+ trio_test_contextvar.set("main")
812
+
813
+ def thread_fn() -> tuple[str, str, str, str, str]:
814
+ thread_parent_value = trio_test_contextvar.get()
815
+ trio_test_contextvar.set("worker")
816
+ thread_current_value = trio_test_contextvar.get()
817
+ with pytest.raises(sniffio.AsyncLibraryNotFoundError):
818
+ sniffio.current_async_library()
819
+
820
+ async def async_back_in_main() -> tuple[str, str]:
821
+ back_parent_value = trio_test_contextvar.get()
822
+ trio_test_contextvar.set("back_in_main")
823
+ back_current_value = trio_test_contextvar.get()
824
+ assert sniffio.current_async_library() == "trio"
825
+ return back_parent_value, back_current_value
826
+
827
+ back_parent_value, back_current_value = from_thread_run(async_back_in_main)
828
+ thread_after_value = trio_test_contextvar.get()
829
+ with pytest.raises(sniffio.AsyncLibraryNotFoundError):
830
+ sniffio.current_async_library()
831
+ return (
832
+ thread_parent_value,
833
+ thread_current_value,
834
+ thread_after_value,
835
+ back_parent_value,
836
+ back_current_value,
837
+ )
838
+
839
+ (
840
+ thread_parent_value,
841
+ thread_current_value,
842
+ thread_after_value,
843
+ back_parent_value,
844
+ back_current_value,
845
+ ) = await to_thread_run_sync(thread_fn)
846
+ current_value = trio_test_contextvar.get()
847
+ assert current_value == thread_parent_value == "main"
848
+ assert thread_current_value == back_parent_value == thread_after_value == "worker"
849
+ assert back_current_value == "back_in_main"
850
+ assert sniffio.current_async_library() == "trio"
851
+
852
+
853
+ def test_run_fn_as_system_task_caught_badly_typed_token() -> None:
854
+ with pytest.raises(RuntimeError):
855
+ from_thread_run_sync(
856
+ _core.current_time,
857
+ trio_token="Not TrioTokentype", # type: ignore[arg-type]
858
+ )
859
+
860
+
861
+ async def test_from_thread_inside_trio_thread() -> None:
862
+ def not_called() -> None: # pragma: no cover
863
+ raise AssertionError()
864
+
865
+ trio_token = _core.current_trio_token()
866
+ with pytest.raises(RuntimeError):
867
+ from_thread_run_sync(not_called, trio_token=trio_token)
868
+
869
+
870
+ def test_from_thread_run_during_shutdown() -> None:
871
+ save = []
872
+ record = []
873
+
874
+ async def agen(token: _core.TrioToken | None) -> AsyncGenerator[None, None]:
875
+ try:
876
+ yield
877
+ finally:
878
+ with _core.CancelScope(shield=True):
879
+ try:
880
+ await to_thread_run_sync(
881
+ partial(from_thread_run, sleep, 0, trio_token=token),
882
+ )
883
+ except _core.RunFinishedError:
884
+ record.append("finished")
885
+ else:
886
+ record.append("clean")
887
+
888
+ async def main(use_system_task: bool) -> None:
889
+ save.append(agen(_core.current_trio_token() if use_system_task else None))
890
+ await save[-1].asend(None)
891
+
892
+ _core.run(main, True) # System nursery will be closed and raise RunFinishedError
893
+ _core.run(main, False) # host task will be rescheduled as normal
894
+ assert record == ["finished", "clean"]
895
+
896
+
897
+ async def test_trio_token_weak_referenceable() -> None:
898
+ token = _core.current_trio_token()
899
+ assert isinstance(token, _core.TrioToken)
900
+ weak_reference = weakref.ref(token)
901
+ assert token is weak_reference()
902
+
903
+
904
+ async def test_unsafe_abandon_on_cancel_kwarg() -> None:
905
+ # This is a stand in for a numpy ndarray or other objects
906
+ # that (maybe surprisingly) lack a notion of truthiness
907
+ class BadBool:
908
+ def __bool__(self) -> bool:
909
+ raise NotImplementedError
910
+
911
+ with pytest.raises(NotImplementedError):
912
+ await to_thread_run_sync(int, abandon_on_cancel=BadBool()) # type: ignore[arg-type]
913
+
914
+
915
+ async def test_from_thread_reuses_task() -> None:
916
+ task = _core.current_task()
917
+
918
+ async def async_current_task() -> _core.Task:
919
+ return _core.current_task()
920
+
921
+ assert task is await to_thread_run_sync(from_thread_run_sync, _core.current_task)
922
+ assert task is await to_thread_run_sync(from_thread_run, async_current_task)
923
+
924
+
925
+ async def test_recursive_to_thread() -> None:
926
+ tid = None
927
+
928
+ def get_tid_then_reenter() -> int:
929
+ nonlocal tid
930
+ tid = threading.get_ident()
931
+ return from_thread_run(to_thread_run_sync, threading.get_ident)
932
+
933
+ assert tid != await to_thread_run_sync(get_tid_then_reenter)
934
+
935
+
936
+ async def test_from_thread_host_cancelled() -> None:
937
+ queue: stdlib_queue.Queue[bool] = stdlib_queue.Queue()
938
+
939
+ def sync_check() -> None:
940
+ from_thread_run_sync(cancel_scope.cancel)
941
+ try:
942
+ from_thread_run_sync(bool)
943
+ except _core.Cancelled: # pragma: no cover
944
+ queue.put(True) # sync functions don't raise Cancelled
945
+ else:
946
+ queue.put(False)
947
+
948
+ with _core.CancelScope() as cancel_scope:
949
+ await to_thread_run_sync(sync_check)
950
+
951
+ assert not cancel_scope.cancelled_caught
952
+ assert not queue.get_nowait()
953
+
954
+ with _core.CancelScope() as cancel_scope:
955
+ await to_thread_run_sync(sync_check, abandon_on_cancel=True)
956
+
957
+ assert cancel_scope.cancelled_caught
958
+ assert not await to_thread_run_sync(partial(queue.get, timeout=1))
959
+
960
+ async def no_checkpoint() -> bool:
961
+ return True
962
+
963
+ def async_check() -> None:
964
+ from_thread_run_sync(cancel_scope.cancel)
965
+ try:
966
+ assert from_thread_run(no_checkpoint)
967
+ except _core.Cancelled: # pragma: no cover
968
+ queue.put(True) # async functions raise Cancelled at checkpoints
969
+ else:
970
+ queue.put(False)
971
+
972
+ with _core.CancelScope() as cancel_scope:
973
+ await to_thread_run_sync(async_check)
974
+
975
+ assert not cancel_scope.cancelled_caught
976
+ assert not queue.get_nowait()
977
+
978
+ with _core.CancelScope() as cancel_scope:
979
+ await to_thread_run_sync(async_check, abandon_on_cancel=True)
980
+
981
+ assert cancel_scope.cancelled_caught
982
+ assert not await to_thread_run_sync(partial(queue.get, timeout=1))
983
+
984
+ async def async_time_bomb() -> None:
985
+ cancel_scope.cancel()
986
+ with fail_after(10):
987
+ await sleep_forever()
988
+
989
+ with _core.CancelScope() as cancel_scope:
990
+ await to_thread_run_sync(from_thread_run, async_time_bomb)
991
+
992
+ assert cancel_scope.cancelled_caught
993
+
994
+
995
+ async def child(
996
+ abandon_on_cancel: bool,
997
+ scope: CancelScope,
998
+ record: list[str],
999
+ f: Callable[[], None],
1000
+ ) -> None:
1001
+ with scope:
1002
+ record.append("start")
1003
+ try:
1004
+ return await to_thread_run_sync(f, abandon_on_cancel=abandon_on_cancel)
1005
+ except _core.Cancelled as e:
1006
+ record.append(str(e))
1007
+ raise
1008
+ finally:
1009
+ record.append("exit")
1010
+
1011
+
1012
+ @pytest.mark.parametrize("cancel_the_scope", [False, True])
1013
+ async def test_from_thread_check_cancelled_no_abandon(cancel_the_scope: bool) -> None:
1014
+ q: stdlib_queue.Queue[str | BaseException] = stdlib_queue.Queue()
1015
+
1016
+ def f() -> None:
1017
+ try:
1018
+ from_thread_check_cancelled()
1019
+ except _core.Cancelled as e: # pragma: no cover, test failure path
1020
+ q.put(str(e))
1021
+ else:
1022
+ q.put("Not Cancelled")
1023
+ ev.wait()
1024
+ return from_thread_check_cancelled()
1025
+
1026
+ record: list[str] = []
1027
+ ev = threading.Event()
1028
+ scope = _core.CancelScope() # Nursery cancel scope gives false positives
1029
+
1030
+ async with _core.open_nursery() as nursery:
1031
+ nursery.start_soon(child, False, scope, record, f)
1032
+ await wait_all_tasks_blocked()
1033
+ assert record[0] == "start"
1034
+ assert q.get(timeout=1) == "Not Cancelled"
1035
+ if cancel_the_scope:
1036
+ scope.cancel()
1037
+ ev.set()
1038
+ # Base case: nothing cancelled so we shouldn't see cancels anywhere
1039
+ if not cancel_the_scope:
1040
+ # implicit assertion, Cancelled not raised via nursery
1041
+ assert record[1] == "exit"
1042
+ else:
1043
+ # abandon_on_cancel=False case: a cancel will pop out but be handled by
1044
+ # the appropriate cancel scope
1045
+
1046
+ assert scope.cancelled_caught
1047
+ assert re.fullmatch(
1048
+ r"cancelled due to explicit from task "
1049
+ r"<Task 'trio._tests.test_threads.test_from_thread_check_cancelled_no_abandon' at 0x\w*>",
1050
+ record[1],
1051
+ ), record[1]
1052
+ assert record[2] == "exit"
1053
+ assert len(record) == 3
1054
+
1055
+
1056
+ async def test_from_thread_check_cancelled_abandon_on_cancel() -> None:
1057
+ q: stdlib_queue.Queue[str | BaseException] = stdlib_queue.Queue()
1058
+ # abandon_on_cancel=True case: slightly different thread behavior needed
1059
+ # check thread is cancelled "soon" after abandonment
1060
+
1061
+ def f() -> None:
1062
+ ev.wait()
1063
+ try:
1064
+ from_thread_check_cancelled()
1065
+ except _core.Cancelled as e:
1066
+ q.put(str(e))
1067
+ except BaseException as e: # pragma: no cover, test failure path
1068
+ # abandon_on_cancel=True will eat exceptions, so we pass it
1069
+ # through the queue in order to be able to debug any exceptions
1070
+ q.put(e)
1071
+ else: # pragma: no cover, test failure path
1072
+ q.put("Not Cancelled")
1073
+
1074
+ record: list[str] = []
1075
+ ev = threading.Event()
1076
+ scope = _core.CancelScope()
1077
+ async with _core.open_nursery() as nursery:
1078
+ nursery.start_soon(child, True, scope, record, f)
1079
+ await wait_all_tasks_blocked()
1080
+ assert record[0] == "start"
1081
+ scope.cancel()
1082
+ # In the worst case the nursery fully exits before the threaded function
1083
+ # checks for cancellation.
1084
+ ev.set()
1085
+
1086
+ assert scope.cancelled_caught
1087
+ assert re.fullmatch(
1088
+ r"cancelled due to explicit from task "
1089
+ r"<Task 'trio._tests.test_threads.test_from_thread_check_cancelled_abandon_on_cancel' at 0x\w*>",
1090
+ record[1],
1091
+ ), record[1]
1092
+ assert record[-1] == "exit"
1093
+ res = q.get(timeout=1)
1094
+ if isinstance(res, BaseException): # pragma: no cover # for debugging
1095
+ raise res
1096
+ else:
1097
+ assert re.fullmatch(
1098
+ r"cancelled due to explicit from task "
1099
+ r"<Task 'trio._tests.test_threads.test_from_thread_check_cancelled_abandon_on_cancel' at 0x\w*>",
1100
+ res,
1101
+ ), res
1102
+
1103
+
1104
+ def test_from_thread_check_cancelled_raises_in_foreign_threads() -> None:
1105
+ with pytest.raises(RuntimeError):
1106
+ from_thread_check_cancelled()
1107
+ q: stdlib_queue.Queue[Outcome[object]] = stdlib_queue.Queue()
1108
+ _core.start_thread_soon(from_thread_check_cancelled, lambda _: q.put(_))
1109
+ with pytest.raises(RuntimeError):
1110
+ q.get(timeout=1).unwrap()
1111
+
1112
+
1113
+ @slow
1114
+ async def test_reentry_doesnt_deadlock() -> None:
1115
+ # Regression test for issue noticed in GH-2827
1116
+ # The failure mode is to hang the whole test suite, unfortunately.
1117
+ # XXX consider running this in a subprocess with a timeout, if it comes up again!
1118
+
1119
+ async def child() -> None:
1120
+ while True:
1121
+ await to_thread_run_sync(from_thread_run, sleep, 0, abandon_on_cancel=False)
1122
+
1123
+ with move_on_after(2):
1124
+ async with _core.open_nursery() as nursery:
1125
+ for _ in range(4):
1126
+ nursery.start_soon(child)
1127
+
1128
+
1129
+ async def test_wait_all_threads_completed() -> None:
1130
+ no_threads_left = False
1131
+ e1 = Event()
1132
+ e2 = Event()
1133
+
1134
+ e1_exited = Event()
1135
+ e2_exited = Event()
1136
+
1137
+ async def wait_event(e: Event, e_exit: Event) -> None:
1138
+ def thread() -> None:
1139
+ from_thread_run(e.wait)
1140
+
1141
+ await to_thread_run_sync(thread)
1142
+ e_exit.set()
1143
+
1144
+ async def wait_no_threads_left() -> None:
1145
+ nonlocal no_threads_left
1146
+ await wait_all_threads_completed()
1147
+ no_threads_left = True
1148
+
1149
+ async with _core.open_nursery() as nursery:
1150
+ nursery.start_soon(wait_event, e1, e1_exited)
1151
+ nursery.start_soon(wait_event, e2, e2_exited)
1152
+ await wait_all_tasks_blocked()
1153
+ nursery.start_soon(wait_no_threads_left)
1154
+ await wait_all_tasks_blocked()
1155
+ assert not no_threads_left
1156
+ assert active_thread_count() == 2
1157
+
1158
+ e1.set()
1159
+ await e1_exited.wait()
1160
+ await wait_all_tasks_blocked()
1161
+ assert not no_threads_left
1162
+ assert active_thread_count() == 1
1163
+
1164
+ e2.set()
1165
+ await e2_exited.wait()
1166
+ await wait_all_tasks_blocked()
1167
+ assert no_threads_left
1168
+ assert active_thread_count() == 0
1169
+
1170
+
1171
+ async def test_wait_all_threads_completed_no_threads() -> None:
1172
+ await wait_all_threads_completed()
1173
+ assert active_thread_count() == 0