omlish 0.0.0.dev195__py3-none-any.whl → 0.0.0.dev197__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 (52) hide show
  1. omlish/__about__.py +3 -3
  2. omlish/asyncs/asyncio/all.py +0 -1
  3. omlish/asyncs/asyncio/asyncio.py +2 -6
  4. omlish/asyncs/bluelet/runner.py +1 -1
  5. omlish/asyncs/bridge.py +2 -2
  6. omlish/codecs/base.py +5 -5
  7. omlish/codecs/text.py +1 -2
  8. omlish/io/compress/adapters.py +4 -4
  9. omlish/io/compress/base.py +4 -4
  10. omlish/io/compress/bz2.py +4 -4
  11. omlish/io/compress/codecs.py +2 -2
  12. omlish/io/compress/gzip.py +10 -10
  13. omlish/io/compress/lz4.py +5 -5
  14. omlish/io/compress/lzma.py +4 -4
  15. omlish/io/compress/zlib.py +4 -4
  16. omlish/io/coro/__init__.py +56 -0
  17. omlish/io/coro/direct.py +13 -0
  18. omlish/io/{generators → coro}/readers.py +31 -31
  19. omlish/io/{generators → coro}/stepped.py +28 -28
  20. omlish/multiprocessing/__init__.py +32 -0
  21. omlish/{multiprocessing.py → multiprocessing/death.py} +3 -88
  22. omlish/multiprocessing/proxies.py +30 -0
  23. omlish/multiprocessing/spawn.py +59 -0
  24. omlish/os/atomics.py +2 -2
  25. omlish/outcome.py +250 -0
  26. omlish/sockets/server.py +1 -2
  27. omlish/term/vt100/terminal.py +1 -1
  28. omlish/testing/pytest/__init__.py +0 -4
  29. omlish/testing/pytest/plugins/asyncs/__init__.py +1 -0
  30. omlish/testing/pytest/plugins/asyncs/backends/__init__.py +16 -0
  31. omlish/testing/pytest/plugins/asyncs/backends/asyncio.py +35 -0
  32. omlish/testing/pytest/plugins/asyncs/backends/base.py +30 -0
  33. omlish/testing/pytest/plugins/asyncs/backends/trio.py +91 -0
  34. omlish/testing/pytest/plugins/asyncs/backends/trio_asyncio.py +89 -0
  35. omlish/testing/pytest/plugins/asyncs/consts.py +3 -0
  36. omlish/testing/pytest/plugins/asyncs/fixtures.py +273 -0
  37. omlish/testing/pytest/plugins/asyncs/plugin.py +182 -0
  38. omlish/testing/pytest/plugins/asyncs/utils.py +10 -0
  39. omlish/testing/pytest/plugins/managermarks.py +0 -14
  40. omlish/text/indent.py +1 -1
  41. omlish/text/minja.py +2 -2
  42. {omlish-0.0.0.dev195.dist-info → omlish-0.0.0.dev197.dist-info}/METADATA +5 -5
  43. {omlish-0.0.0.dev195.dist-info → omlish-0.0.0.dev197.dist-info}/RECORD +48 -36
  44. {omlish-0.0.0.dev195.dist-info → omlish-0.0.0.dev197.dist-info}/WHEEL +1 -1
  45. omlish/io/generators/__init__.py +0 -56
  46. omlish/io/generators/direct.py +0 -13
  47. omlish/testing/pytest/marks.py +0 -18
  48. omlish/testing/pytest/plugins/asyncs.py +0 -162
  49. /omlish/io/{generators → coro}/consts.py +0 -0
  50. {omlish-0.0.0.dev195.dist-info → omlish-0.0.0.dev197.dist-info}/LICENSE +0 -0
  51. {omlish-0.0.0.dev195.dist-info → omlish-0.0.0.dev197.dist-info}/entry_points.txt +0 -0
  52. {omlish-0.0.0.dev195.dist-info → omlish-0.0.0.dev197.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,35 @@
1
+ import functools
2
+ import sys
3
+ import typing as ta
4
+
5
+ from ...... import check
6
+ from ...... import lang
7
+ from .base import AsyncsBackend
8
+
9
+
10
+ if ta.TYPE_CHECKING:
11
+ import asyncio
12
+ else:
13
+ asyncio = lang.proxy_import('asyncio')
14
+
15
+
16
+ class AsyncioAsyncsBackend(AsyncsBackend):
17
+ name = 'asyncio'
18
+
19
+ def is_available(self) -> bool:
20
+ return True
21
+
22
+ def is_imported(self) -> bool:
23
+ return 'asyncio' in sys.modules
24
+
25
+ #
26
+
27
+ def wrap_runner(self, fn):
28
+ @functools.wraps(fn)
29
+ def wrapper(**kwargs):
30
+ with asyncio.Runner(loop_factory=asyncio.get_event_loop_policy().new_event_loop) as runner:
31
+ loop_cls = type(runner.get_loop())
32
+ check.equal(loop_cls.__module__.split('.')[0], 'asyncio')
33
+ return runner.run(fn(**kwargs))
34
+
35
+ return wrapper
@@ -0,0 +1,30 @@
1
+ import abc
2
+
3
+
4
+ class AsyncsBackend(abc.ABC):
5
+ @property
6
+ @abc.abstractmethod
7
+ def name(self) -> str:
8
+ raise NotImplementedError
9
+
10
+ @abc.abstractmethod
11
+ def is_available(self) -> bool:
12
+ raise NotImplementedError
13
+
14
+ @abc.abstractmethod
15
+ def is_imported(self) -> bool:
16
+ raise NotImplementedError
17
+
18
+ #
19
+
20
+ def prepare_for_metafunc(self, metafunc) -> None: # noqa
21
+ pass
22
+
23
+ #
24
+
25
+ @abc.abstractmethod
26
+ def wrap_runner(self, fn):
27
+ raise NotImplementedError
28
+
29
+ async def install_context(self, contextvars_ctx): # noqa
30
+ pass
@@ -0,0 +1,91 @@
1
+ # Based on pytest-trio, licensed under the MIT license, duplicated below.
2
+ #
3
+ # https://github.com/python-trio/pytest-trio/tree/cd6cc14b061d34f35980e38c44052108ed5402d1
4
+ #
5
+ # The MIT License (MIT)
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
8
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
9
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
10
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
13
+ # Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
16
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
18
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19
+ import functools
20
+ import sys
21
+ import typing as ta
22
+
23
+ import pytest
24
+ from _pytest.outcomes import Skipped # noqa
25
+ from _pytest.outcomes import XFailed # noqa
26
+
27
+ from ...... import lang
28
+ from .base import AsyncsBackend
29
+
30
+
31
+ if ta.TYPE_CHECKING:
32
+ import trio
33
+ else:
34
+ trio = lang.proxy_import('trio', extras=['abc'])
35
+
36
+
37
+ class TrioAsyncsBackend(AsyncsBackend):
38
+ name = 'trio'
39
+
40
+ def is_available(self) -> bool:
41
+ return lang.can_import('trio')
42
+
43
+ def is_imported(self) -> bool:
44
+ return 'trio' in sys.modules
45
+
46
+ #
47
+
48
+ def wrap_runner(self, fn):
49
+ @functools.wraps(fn)
50
+ def wrapper(**kwargs):
51
+ __tracebackhide__ = True
52
+
53
+ clocks = {k: c for k, c in kwargs.items() if isinstance(c, trio.abc.Clock)}
54
+ if not clocks:
55
+ clock = None
56
+ elif len(clocks) == 1:
57
+ clock = list(clocks.values())[0] # noqa
58
+ else:
59
+ raise ValueError(f'Expected at most one Clock in kwargs, got {clocks!r}')
60
+
61
+ instruments = [i for i in kwargs.values() if isinstance(i, trio.abc.Instrument)]
62
+
63
+ try:
64
+ return trio.run(
65
+ functools.partial(fn, **kwargs),
66
+ clock=clock,
67
+ instruments=instruments,
68
+ )
69
+
70
+ except BaseExceptionGroup as eg:
71
+ queue: list[BaseException] = [eg]
72
+ leaves = []
73
+
74
+ while queue:
75
+ ex = queue.pop()
76
+ if isinstance(ex, BaseExceptionGroup):
77
+ queue.extend(ex.exceptions)
78
+ else:
79
+ leaves.append(ex)
80
+
81
+ if len(leaves) == 1:
82
+ if isinstance(leaves[0], XFailed):
83
+ pytest.xfail()
84
+ if isinstance(leaves[0], Skipped):
85
+ pytest.skip()
86
+
87
+ # Since our leaf exceptions don't consist of exactly one 'magic' skipped or xfailed exception, re-raise
88
+ # the whole group.
89
+ raise
90
+
91
+ return wrapper
@@ -0,0 +1,89 @@
1
+ # Based on pytest-trio, licensed under the MIT license, duplicated below.
2
+ #
3
+ # https://github.com/python-trio/pytest-trio/tree/cd6cc14b061d34f35980e38c44052108ed5402d1
4
+ #
5
+ # The MIT License (MIT)
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
8
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
9
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
10
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
13
+ # Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
16
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
18
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19
+ import functools
20
+ import sys
21
+ import typing as ta
22
+
23
+ from _pytest.outcomes import Skipped # noqa
24
+ from _pytest.outcomes import XFailed # noqa
25
+
26
+ from ...... import cached
27
+ from ...... import lang
28
+ from ......diag import pydevd as pdu
29
+ from .base import AsyncsBackend
30
+
31
+
32
+ if ta.TYPE_CHECKING:
33
+ import trio_asyncio
34
+ else:
35
+ trio = lang.proxy_import('trio', extras=['abc'])
36
+ trio_asyncio = lang.proxy_import('trio_asyncio')
37
+
38
+
39
+ class TrioAsyncioAsyncsBackend(AsyncsBackend):
40
+ name = 'trio_asyncio'
41
+
42
+ def is_available(self) -> bool:
43
+ return lang.can_import('trio_asyncio')
44
+
45
+ def is_imported(self) -> bool:
46
+ return 'trio_asyncio' in sys.modules
47
+
48
+ #
49
+
50
+ @cached.function
51
+ def _prepare(self) -> None:
52
+ # NOTE: Importing it here is apparently necessary to get its patching working - otherwise fails later with
53
+ # `no running event loop` in anyio._backends._asyncio and such.
54
+ import trio_asyncio # noqa
55
+
56
+ if pdu.is_present():
57
+ pdu.patch_for_trio_asyncio()
58
+
59
+ def prepare_for_metafunc(self, metafunc) -> None:
60
+ self._prepare()
61
+
62
+ #
63
+
64
+ def wrap_runner(self, fn):
65
+ @functools.wraps(fn)
66
+ def wrapper(**kwargs):
67
+ return trio_asyncio.run(
68
+ trio_asyncio.aio_as_trio(
69
+ functools.partial(fn, **kwargs),
70
+ ),
71
+ )
72
+
73
+ return wrapper
74
+
75
+ async def install_context(self, contextvars_ctx):
76
+ # Seemingly no longer necessary?
77
+ # https://github.com/python-trio/pytest-trio/commit/ef0cd267ea62188a8e475c66cb584e7a2addc02a
78
+
79
+ # # This is a gross hack. I guess Trio should provide a context= argument to start_soon/start?
80
+ # task = trio.lowlevel.current_task()
81
+ # if CANARY in task.context:
82
+ # return
83
+
84
+ # task.context = contextvars_ctx
85
+
86
+ # # Force a yield so we pick up the new context
87
+ # await trio.sleep(0)
88
+
89
+ pass
@@ -0,0 +1,3 @@
1
+ ASYNCS_MARK = 'asyncs'
2
+
3
+ PARAM_NAME = '__async_backend'
@@ -0,0 +1,273 @@
1
+ # Based on pytest-trio, licensed under the MIT license, duplicated below.
2
+ #
3
+ # https://github.com/python-trio/pytest-trio/tree/cd6cc14b061d34f35980e38c44052108ed5402d1
4
+ #
5
+ # The MIT License (MIT)
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
8
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
9
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
10
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
13
+ # Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
16
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
18
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19
+ import collections.abc
20
+ import contextlib
21
+ import contextvars
22
+ import inspect
23
+ import typing as ta
24
+
25
+ from _pytest.outcomes import Skipped # noqa
26
+ from _pytest.outcomes import XFailed # noqa
27
+
28
+ from ..... import check
29
+ from ..... import lang
30
+ from ..... import outcome
31
+ from .backends.base import AsyncsBackend
32
+ from .utils import is_coroutine_function
33
+
34
+
35
+ if ta.TYPE_CHECKING:
36
+ import anyio.abc
37
+ else:
38
+ anyio = lang.proxy_import('anyio', extras=['abc'])
39
+
40
+
41
+ ##
42
+
43
+
44
+ CANARY: contextvars.ContextVar[ta.Any] = contextvars.ContextVar('pytest-omlish-asyncs canary')
45
+
46
+
47
+ class NURSERY_FIXTURE_PLACEHOLDER: # noqa
48
+ pass
49
+
50
+
51
+ ##
52
+
53
+
54
+ class AsyncsTestContext:
55
+ def __init__(self, backend: AsyncsBackend) -> None:
56
+ super().__init__()
57
+
58
+ self.backend = backend
59
+
60
+ self.crashed = False
61
+
62
+ # This holds cancel scopes for whatever setup steps are currently running -- initially it's the fixtures that
63
+ # are in the middle of evaluating themselves, and then once fixtures are set up it's the test itself. Basically,
64
+ # at any given moment, it's the stuff we need to cancel if we want to start tearing down our fixture DAG.
65
+ self.active_cancel_scopes: set[anyio.CancelScope] = set()
66
+
67
+ self.fixtures_with_errors: set[AsyncsFixture] = set()
68
+ self.fixtures_with_cancel: set[AsyncsFixture] = set()
69
+
70
+ self.error_list: list[BaseException] = []
71
+
72
+ def crash(self, fixture: 'AsyncsFixture', exc: BaseException | None) -> None:
73
+ if exc is None:
74
+ self.fixtures_with_cancel.add(fixture)
75
+ else:
76
+ self.error_list.append(exc)
77
+ self.fixtures_with_errors.add(fixture)
78
+
79
+ self.crashed = True
80
+
81
+ for cscope in self.active_cancel_scopes:
82
+ cscope.cancel()
83
+
84
+
85
+ ##
86
+
87
+
88
+ class AsyncsFixture:
89
+ """
90
+ Represent a fixture that need to be run in a async context to be resolved.
91
+
92
+ The name is actually a misnomer, because we use it to represent the actual test itself as well, since the
93
+ test is basically just a fixture with no dependents and no teardown.
94
+ """
95
+
96
+ def __init__(
97
+ self,
98
+ name: str,
99
+ func: ta.Callable,
100
+ pytest_kwargs: ta.Mapping[str, ta.Any],
101
+ *,
102
+ is_test: bool = False,
103
+ ) -> None:
104
+ super().__init__()
105
+
106
+ self.name = name
107
+ self._func = func
108
+ self._pytest_kwargs = pytest_kwargs
109
+ self._is_test = is_test
110
+ self._teardown_done = anyio.Event()
111
+
112
+ # These attrs are all accessed from other objects: Downstream users read this value.
113
+ self.fixture_value: ta.Any = None
114
+
115
+ # This event notifies downstream users that we're done setting up. Invariant: if this is set, then either
116
+ # fixture_value is usable *or* test_ctx.crashed is True.
117
+ self.setup_done = anyio.Event()
118
+
119
+ # Downstream users *modify* this value, by adding their _teardown_done events to it, so we know who we need to
120
+ # wait for before tearing down.
121
+ self.user_done_events: set[anyio.Event] = set()
122
+
123
+ def register_and_collect_dependencies(self) -> set['AsyncsFixture']:
124
+ # Returns the set of all AsyncsFixtures that this fixture depends on, directly or indirectly, and sets up all
125
+ # their user_done_events.
126
+ deps = set()
127
+ deps.add(self)
128
+ for value in self._pytest_kwargs.values():
129
+ if isinstance(value, AsyncsFixture):
130
+ value.user_done_events.add(self._teardown_done)
131
+ deps.update(value.register_and_collect_dependencies())
132
+ return deps
133
+
134
+ @contextlib.asynccontextmanager
135
+ async def _fixture_manager(self, test_ctx: AsyncsTestContext) -> ta.AsyncIterator['anyio.abc.TaskGroup']:
136
+ __tracebackhide__ = True
137
+
138
+ try:
139
+ async with anyio.create_task_group() as nursery_fixture:
140
+ try:
141
+ yield nursery_fixture
142
+ finally:
143
+ nursery_fixture.cancel_scope.cancel()
144
+
145
+ except BaseException as exc: # noqa
146
+ test_ctx.crash(self, exc)
147
+
148
+ finally:
149
+ self.setup_done.set()
150
+ self._teardown_done.set()
151
+
152
+ async def run(
153
+ self,
154
+ test_ctx: AsyncsTestContext,
155
+ contextvars_ctx: contextvars.Context,
156
+ ) -> None:
157
+ __tracebackhide__ = True
158
+
159
+ await test_ctx.backend.install_context(contextvars_ctx)
160
+
161
+ # Check that it worked, since technically trio doesn't *guarantee* that sleep(0) will actually yield.
162
+ check.equal(CANARY.get(), 'in correct context')
163
+
164
+ # This 'with' block handles the nursery fixture lifetime, the teardown_done event, and crashing the context if
165
+ # there's an unhandled exception.
166
+ async with self._fixture_manager(test_ctx) as nursery_fixture:
167
+ # Resolve our kwargs
168
+ resolved_kwargs: dict = {}
169
+ for name, value in self._pytest_kwargs.items():
170
+ if isinstance(value, AsyncsFixture):
171
+ await value.setup_done.wait()
172
+ if value.fixture_value is NURSERY_FIXTURE_PLACEHOLDER:
173
+ resolved_kwargs[name] = nursery_fixture
174
+ else:
175
+ resolved_kwargs[name] = value.fixture_value
176
+ else:
177
+ resolved_kwargs[name] = value
178
+
179
+ # If something's already crashed before we're ready to start, then there's no point in even setting up.
180
+ if test_ctx.crashed:
181
+ return
182
+
183
+ # Run actual fixture setup step. If another fixture crashes while we're in the middle of setting up, we want
184
+ # to be cancelled immediately, so we'll save an encompassing cancel scope where self._crash can find it.
185
+ test_ctx.active_cancel_scopes.add(nursery_fixture.cancel_scope)
186
+ if self._is_test:
187
+ # Tests are exactly like fixtures, except that they to be
188
+ # regular async functions.
189
+ check.state(not self.user_done_events)
190
+ func_value = None
191
+ check.state(not test_ctx.crashed)
192
+ await self._func(**resolved_kwargs)
193
+
194
+ else:
195
+ func_value = self._func(**resolved_kwargs)
196
+ if isinstance(func_value, collections.abc.Coroutine):
197
+ self.fixture_value = await func_value
198
+ elif inspect.isasyncgen(func_value):
199
+ self.fixture_value = await func_value.asend(None)
200
+ elif isinstance(func_value, collections.abc.Generator):
201
+ self.fixture_value = func_value.send(None)
202
+ else:
203
+ # Regular synchronous function
204
+ self.fixture_value = func_value
205
+
206
+ # Now that we're done setting up, we don't want crashes to cancel us immediately; instead we want them to
207
+ # cancel our downstream dependents, and then eventually let us clean up normally. So remove this from the
208
+ # set of cancel scopes affected by self._crash.
209
+ test_ctx.active_cancel_scopes.remove(nursery_fixture.cancel_scope)
210
+
211
+ # self.fixture_value is ready, so notify users that they can continue. (Or, maybe we crashed and were
212
+ # cancelled, in which case our users will check test_ctx.crashed and immediately exit, which is fine too.)
213
+ self.setup_done.set()
214
+
215
+ # Wait for users to be finished.
216
+ #
217
+ # At this point we're in a very strange state: if the fixture yielded inside a nursery or cancel scope, then
218
+ # we are still "inside" that scope even though its with block is not on the stack. In particular this means
219
+ # that if they get cancelled, then our waiting might get a Cancelled error, that we cannot really deal with
220
+ # - it should get thrown back into the fixture generator, but pytest fixture generators don't work that way:
221
+ # https://github.com/python-trio/pytest-trio/issues/55
222
+ # And besides, we can't start tearing down until all our users have finished.
223
+ #
224
+ # So if we get an exception here, we crash the context (which cancels the test and starts the cleanup
225
+ # process), save any exception that *isn't* Cancelled (because if its Cancelled then we can't route it to
226
+ # the right place, and anyway the teardown code will get it again if it matters), and then use a shield to
227
+ # keep waiting for the teardown to finish without having to worry about cancellation.
228
+ yield_outcome: outcome.Outcome = outcome.Value(None)
229
+ try:
230
+ for event in self.user_done_events:
231
+ await event.wait()
232
+
233
+ except BaseException as exc: # noqa
234
+ check.isinstance(exc, anyio.get_cancelled_exc_class())
235
+ yield_outcome = outcome.Error(exc)
236
+ test_ctx.crash(self, None)
237
+ with anyio.CancelScope(shield=True):
238
+ for event in self.user_done_events:
239
+ await event.wait()
240
+
241
+ # Do our teardown
242
+ if inspect.isasyncgen(func_value):
243
+ try:
244
+ await yield_outcome.asend(func_value)
245
+ except StopAsyncIteration:
246
+ pass
247
+ else:
248
+ raise RuntimeError('too many yields in fixture')
249
+
250
+ elif isinstance(func_value, collections.abc.Generator):
251
+ try:
252
+ yield_outcome.send(func_value)
253
+ except StopIteration:
254
+ pass
255
+ else:
256
+ raise RuntimeError('too many yields in fixture')
257
+
258
+
259
+ ##
260
+
261
+
262
+ def is_asyncs_fixture(
263
+ func: ta.Callable,
264
+ coerce_async: bool,
265
+ kwargs: ta.Mapping[str, ta.Any],
266
+ ) -> bool:
267
+ if coerce_async and (is_coroutine_function(func) or inspect.isasyncgenfunction(func)):
268
+ return True
269
+
270
+ if any(isinstance(value, AsyncsFixture) for value in kwargs.values()):
271
+ return True
272
+
273
+ return False