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.
- omlish/__about__.py +3 -3
- omlish/asyncs/bluelet/runner.py +1 -1
- omlish/codecs/base.py +5 -5
- omlish/codecs/text.py +1 -2
- omlish/io/compress/adapters.py +4 -4
- omlish/io/compress/base.py +4 -4
- omlish/io/compress/bz2.py +4 -4
- omlish/io/compress/codecs.py +2 -2
- omlish/io/compress/gzip.py +10 -10
- omlish/io/compress/lz4.py +5 -5
- omlish/io/compress/lzma.py +4 -4
- omlish/io/compress/zlib.py +4 -4
- omlish/io/coro/__init__.py +56 -0
- omlish/io/coro/direct.py +13 -0
- omlish/io/{generators → coro}/readers.py +31 -31
- omlish/io/{generators → coro}/stepped.py +28 -28
- omlish/multiprocessing/__init__.py +32 -0
- omlish/{multiprocessing.py → multiprocessing/death.py} +3 -88
- omlish/multiprocessing/proxies.py +30 -0
- omlish/multiprocessing/spawn.py +59 -0
- omlish/os/atomics.py +2 -2
- omlish/outcome.py +250 -0
- omlish/sockets/server.py +1 -2
- omlish/term/vt100/terminal.py +1 -1
- omlish/testing/pytest/plugins/asyncs/__init__.py +1 -0
- omlish/testing/pytest/plugins/asyncs/backends/__init__.py +16 -0
- omlish/testing/pytest/plugins/asyncs/backends/asyncio.py +35 -0
- omlish/testing/pytest/plugins/asyncs/backends/base.py +30 -0
- omlish/testing/pytest/plugins/asyncs/backends/trio.py +91 -0
- omlish/testing/pytest/plugins/asyncs/backends/trio_asyncio.py +89 -0
- omlish/testing/pytest/plugins/asyncs/consts.py +3 -0
- omlish/testing/pytest/plugins/asyncs/fixtures.py +273 -0
- omlish/testing/pytest/plugins/asyncs/plugin.py +182 -0
- omlish/testing/pytest/plugins/asyncs/utils.py +10 -0
- omlish/text/indent.py +1 -1
- omlish/text/minja.py +2 -2
- {omlish-0.0.0.dev194.dist-info → omlish-0.0.0.dev196.dist-info}/METADATA +5 -5
- {omlish-0.0.0.dev194.dist-info → omlish-0.0.0.dev196.dist-info}/RECORD +43 -30
- {omlish-0.0.0.dev194.dist-info → omlish-0.0.0.dev196.dist-info}/WHEEL +1 -1
- omlish/io/generators/__init__.py +0 -56
- omlish/io/generators/direct.py +0 -13
- omlish/testing/pytest/plugins/asyncs.py +0 -162
- /omlish/io/{generators → coro}/consts.py +0 -0
- {omlish-0.0.0.dev194.dist-info → omlish-0.0.0.dev196.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev194.dist-info → omlish-0.0.0.dev196.dist-info}/entry_points.txt +0 -0
- {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,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
omlish/text/minja.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: omlish
|
3
|
-
Version: 0.0.0.
|
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.
|
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.
|
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.
|
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.
|
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"
|