omlish 0.0.0.dev194__py3-none-any.whl → 0.0.0.dev196__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 (46) hide show
  1. omlish/__about__.py +3 -3
  2. omlish/asyncs/bluelet/runner.py +1 -1
  3. omlish/codecs/base.py +5 -5
  4. omlish/codecs/text.py +1 -2
  5. omlish/io/compress/adapters.py +4 -4
  6. omlish/io/compress/base.py +4 -4
  7. omlish/io/compress/bz2.py +4 -4
  8. omlish/io/compress/codecs.py +2 -2
  9. omlish/io/compress/gzip.py +10 -10
  10. omlish/io/compress/lz4.py +5 -5
  11. omlish/io/compress/lzma.py +4 -4
  12. omlish/io/compress/zlib.py +4 -4
  13. omlish/io/coro/__init__.py +56 -0
  14. omlish/io/coro/direct.py +13 -0
  15. omlish/io/{generators → coro}/readers.py +31 -31
  16. omlish/io/{generators → coro}/stepped.py +28 -28
  17. omlish/multiprocessing/__init__.py +32 -0
  18. omlish/{multiprocessing.py → multiprocessing/death.py} +3 -88
  19. omlish/multiprocessing/proxies.py +30 -0
  20. omlish/multiprocessing/spawn.py +59 -0
  21. omlish/os/atomics.py +2 -2
  22. omlish/outcome.py +250 -0
  23. omlish/sockets/server.py +1 -2
  24. omlish/term/vt100/terminal.py +1 -1
  25. omlish/testing/pytest/plugins/asyncs/__init__.py +1 -0
  26. omlish/testing/pytest/plugins/asyncs/backends/__init__.py +16 -0
  27. omlish/testing/pytest/plugins/asyncs/backends/asyncio.py +35 -0
  28. omlish/testing/pytest/plugins/asyncs/backends/base.py +30 -0
  29. omlish/testing/pytest/plugins/asyncs/backends/trio.py +91 -0
  30. omlish/testing/pytest/plugins/asyncs/backends/trio_asyncio.py +89 -0
  31. omlish/testing/pytest/plugins/asyncs/consts.py +3 -0
  32. omlish/testing/pytest/plugins/asyncs/fixtures.py +273 -0
  33. omlish/testing/pytest/plugins/asyncs/plugin.py +182 -0
  34. omlish/testing/pytest/plugins/asyncs/utils.py +10 -0
  35. omlish/text/indent.py +1 -1
  36. omlish/text/minja.py +2 -2
  37. {omlish-0.0.0.dev194.dist-info → omlish-0.0.0.dev196.dist-info}/METADATA +5 -5
  38. {omlish-0.0.0.dev194.dist-info → omlish-0.0.0.dev196.dist-info}/RECORD +43 -30
  39. {omlish-0.0.0.dev194.dist-info → omlish-0.0.0.dev196.dist-info}/WHEEL +1 -1
  40. omlish/io/generators/__init__.py +0 -56
  41. omlish/io/generators/direct.py +0 -13
  42. omlish/testing/pytest/plugins/asyncs.py +0 -162
  43. /omlish/io/{generators → coro}/consts.py +0 -0
  44. {omlish-0.0.0.dev194.dist-info → omlish-0.0.0.dev196.dist-info}/LICENSE +0 -0
  45. {omlish-0.0.0.dev194.dist-info → omlish-0.0.0.dev196.dist-info}/entry_points.txt +0 -0
  46. {omlish-0.0.0.dev194.dist-info → omlish-0.0.0.dev196.dist-info}/top_level.txt +0 -0
@@ -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
@@ -0,0 +1,182 @@
1
+ """
2
+ TODO:
3
+ - auto drain_asyncio
4
+ """
5
+ import contextvars
6
+ import functools
7
+ import sys
8
+ import typing as ta
9
+ import warnings
10
+
11
+ import pytest
12
+ from _pytest.outcomes import Skipped # noqa
13
+ from _pytest.outcomes import XFailed # noqa
14
+
15
+ from ..... import check
16
+ from ..... import lang
17
+ from .._registry import register
18
+ from .backends import ASYNC_BACKENDS
19
+ from .backends import AsyncsBackend
20
+ from .consts import ASYNCS_MARK
21
+ from .consts import PARAM_NAME
22
+ from .fixtures import CANARY
23
+ from .fixtures import AsyncsFixture
24
+ from .fixtures import AsyncsTestContext
25
+ from .fixtures import is_asyncs_fixture
26
+ from .utils import is_async_function
27
+ from .utils import is_coroutine_function
28
+
29
+
30
+ if ta.TYPE_CHECKING:
31
+ import anyio
32
+ else:
33
+ anyio = lang.proxy_import('anyio')
34
+
35
+
36
+ ##
37
+
38
+
39
+ @register
40
+ class AsyncsPlugin:
41
+ def __init__(self, backends: ta.Collection[type[AsyncsBackend]] | None = None) -> None:
42
+ super().__init__()
43
+
44
+ if backends is None:
45
+ backends = ASYNC_BACKENDS
46
+
47
+ bd: dict[str, AsyncsBackend] = {}
48
+ for bc in backends:
49
+ be = bc()
50
+ if not be.is_available():
51
+ continue
52
+ bn = be.name
53
+ check.not_in(bn, bd)
54
+ bd[bn] = be
55
+ self._backends = bd
56
+
57
+ def pytest_cmdline_main(self, config):
58
+ if (aio_plugin := sys.modules.get('pytest_asyncio.plugin')):
59
+ # warnings.filterwarnings is clobbered by pytest using warnings.catch_warnings
60
+ def aio_plugin_warn(message, *args, **kwargs):
61
+ if (
62
+ isinstance(message, pytest.PytestDeprecationWarning) and
63
+ message.args[0].startswith('The configuration option "asyncio_default_fixture_loop_scope" is unset.') # noqa
64
+ ):
65
+ return
66
+ warnings.warn(message, *args, **kwargs)
67
+
68
+ aio_plugin.warnings = lang.proxy_import('warnings') # type: ignore
69
+ aio_plugin.warnings.warn = aio_plugin_warn # type: ignore
70
+
71
+ def pytest_configure(self, config):
72
+ config.addinivalue_line('markers', f'{ASYNCS_MARK}: marks for all async backends')
73
+
74
+ def pytest_generate_tests(self, metafunc):
75
+ if (m := metafunc.definition.get_closest_marker(ASYNCS_MARK)) is not None:
76
+ if m.args:
77
+ bns = m.args
78
+ else:
79
+ bns = list(self._backends)
80
+ else:
81
+ return
82
+
83
+ for bn in bns:
84
+ be = self._backends[bn]
85
+ be.prepare_for_metafunc(metafunc)
86
+
87
+ metafunc.fixturenames.append(PARAM_NAME)
88
+ metafunc.parametrize(PARAM_NAME, bns)
89
+
90
+ def pytest_fixture_setup(self, fixturedef, request):
91
+ is_asyncs_test = request.node.get_closest_marker(ASYNCS_MARK) is not None
92
+
93
+ kwargs = {name: request.getfixturevalue(name) for name in fixturedef.argnames}
94
+
95
+ if not is_asyncs_fixture(fixturedef.func, is_asyncs_test, kwargs):
96
+ return None
97
+
98
+ if request.scope != 'function':
99
+ raise RuntimeError('Asyncs fixtures must be function-scope')
100
+
101
+ if not is_asyncs_test:
102
+ raise RuntimeError('Asyncs fixtures can only be used by Asyncs tests')
103
+
104
+ fixture = AsyncsFixture(
105
+ '<fixture {!r}>'.format(fixturedef.argname), # noqa
106
+ fixturedef.func,
107
+ kwargs,
108
+ )
109
+
110
+ fixturedef.cached_result = (fixture, request.param_index, None)
111
+
112
+ return fixture
113
+
114
+ @pytest.hookimpl(hookwrapper=True)
115
+ def pytest_runtest_call(self, item):
116
+ if (m := item.get_closest_marker(ASYNCS_MARK)) is None: # noqa
117
+ if is_async_function(item.obj):
118
+ from _pytest.unittest import UnitTestCase # noqa
119
+ if isinstance(item.parent, UnitTestCase):
120
+ # unittest handles these itself.
121
+ pass
122
+ else:
123
+ raise Exception(f'{item.nodeid}: async def function and no async plugin specified')
124
+
125
+ yield
126
+ return
127
+
128
+ bn = item.callspec.params[PARAM_NAME]
129
+ be = self._backends[bn]
130
+
131
+ item.obj = self.test_runner_factory(be, item)
132
+
133
+ yield
134
+
135
+ def test_runner_factory(self, backend: AsyncsBackend, item, testfunc=None):
136
+ if not testfunc:
137
+ testfunc = item.obj
138
+
139
+ if not is_coroutine_function(testfunc):
140
+ pytest.fail(f'test function `{item!r}` is marked asyncs but is not async')
141
+
142
+ @backend.wrap_runner
143
+ async def _bootstrap_fixtures_and_run_test(**kwargs):
144
+ __tracebackhide__ = True
145
+
146
+ test_ctx = AsyncsTestContext(backend)
147
+ test = AsyncsFixture(
148
+ '<test {!r}>'.format(testfunc.__name__), # noqa
149
+ testfunc,
150
+ kwargs,
151
+ is_test=True,
152
+ )
153
+
154
+ contextvars_ctx = contextvars.copy_context()
155
+ contextvars_ctx.run(CANARY.set, 'in correct context')
156
+
157
+ async with anyio.create_task_group() as nursery:
158
+ for fixture in test.register_and_collect_dependencies():
159
+ contextvars_ctx.run(
160
+ functools.partial(
161
+ nursery.start_soon,
162
+ fixture.run,
163
+ test_ctx,
164
+ contextvars_ctx,
165
+ name=fixture.name,
166
+ ),
167
+ )
168
+
169
+ silent_cancellers = test_ctx.fixtures_with_cancel - test_ctx.fixtures_with_errors
170
+
171
+ if silent_cancellers:
172
+ for fixture in silent_cancellers:
173
+ test_ctx.error_list.append(
174
+ RuntimeError(f"{fixture.name} cancelled the test but didn't raise an error"),
175
+ )
176
+
177
+ if len(test_ctx.error_list) == 1:
178
+ raise test_ctx.error_list[0]
179
+ elif test_ctx.error_list:
180
+ raise BaseExceptionGroup('errors in async test and async fixtures', test_ctx.error_list)
181
+
182
+ return _bootstrap_fixtures_and_run_test
@@ -0,0 +1,10 @@
1
+ import inspect
2
+ import typing as ta
3
+
4
+
5
+ def is_coroutine_function(func: ta.Any) -> bool:
6
+ return inspect.iscoroutinefunction(func) or getattr(func, '_is_coroutine', False)
7
+
8
+
9
+ def is_async_function(func: ta.Any) -> bool:
10
+ return is_coroutine_function(func) or inspect.isasyncgenfunction(func)
omlish/text/indent.py CHANGED
@@ -4,7 +4,7 @@ import contextlib
4
4
  import io
5
5
  import typing as ta
6
6
 
7
- from omlish.lite.check import check
7
+ from ..lite.check import check
8
8
 
9
9
 
10
10
  class IndentWriter:
omlish/text/minja.py CHANGED
@@ -9,8 +9,8 @@ import io
9
9
  import re
10
10
  import typing as ta
11
11
 
12
- from omlish.lite.cached import cached_nullary
13
- from omlish.lite.check import check
12
+ from ..lite.cached import cached_nullary
13
+ from ..lite.check import check
14
14
 
15
15
 
16
16
  ##
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: omlish
3
- Version: 0.0.0.dev194
3
+ Version: 0.0.0.dev196
4
4
  Summary: omlish
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -13,7 +13,7 @@ Classifier: Operating System :: POSIX
13
13
  Requires-Python: >=3.12
14
14
  License-File: LICENSE
15
15
  Provides-Extra: all
16
- Requires-Dist: anyio~=4.7; extra == "all"
16
+ Requires-Dist: anyio~=4.8; extra == "all"
17
17
  Requires-Dist: sniffio~=1.3; extra == "all"
18
18
  Requires-Dist: greenlet~=3.1; extra == "all"
19
19
  Requires-Dist: trio~=0.27; extra == "all"
@@ -44,7 +44,7 @@ Requires-Dist: apsw~=3.47; extra == "all"
44
44
  Requires-Dist: sqlean.py~=3.45; extra == "all"
45
45
  Requires-Dist: duckdb~=1.1; extra == "all"
46
46
  Requires-Dist: pytest~=8.0; extra == "all"
47
- Requires-Dist: anyio~=4.7; extra == "all"
47
+ Requires-Dist: anyio~=4.8; extra == "all"
48
48
  Requires-Dist: sniffio~=1.3; extra == "all"
49
49
  Requires-Dist: asttokens~=3.0; extra == "all"
50
50
  Requires-Dist: executing~=2.1; extra == "all"
@@ -52,7 +52,7 @@ Requires-Dist: orjson~=3.10; extra == "all"
52
52
  Requires-Dist: pyyaml~=6.0; extra == "all"
53
53
  Requires-Dist: wrapt~=1.14; extra == "all"
54
54
  Provides-Extra: async
55
- Requires-Dist: anyio~=4.7; extra == "async"
55
+ Requires-Dist: anyio~=4.8; extra == "async"
56
56
  Requires-Dist: sniffio~=1.3; extra == "async"
57
57
  Requires-Dist: greenlet~=3.1; extra == "async"
58
58
  Requires-Dist: trio~=0.27; extra == "async"
@@ -93,7 +93,7 @@ Requires-Dist: duckdb~=1.1; extra == "sqldrivers"
93
93
  Provides-Extra: testing
94
94
  Requires-Dist: pytest~=8.0; extra == "testing"
95
95
  Provides-Extra: plus
96
- Requires-Dist: anyio~=4.7; extra == "plus"
96
+ Requires-Dist: anyio~=4.8; extra == "plus"
97
97
  Requires-Dist: sniffio~=1.3; extra == "plus"
98
98
  Requires-Dist: asttokens~=3.0; extra == "plus"
99
99
  Requires-Dist: executing~=2.1; extra == "plus"