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.
- omlish/__about__.py +3 -3
- omlish/asyncs/asyncio/all.py +0 -1
- omlish/asyncs/asyncio/asyncio.py +2 -6
- omlish/asyncs/bluelet/runner.py +1 -1
- omlish/asyncs/bridge.py +2 -2
- 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/__init__.py +0 -4
- 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/testing/pytest/plugins/managermarks.py +0 -14
- omlish/text/indent.py +1 -1
- omlish/text/minja.py +2 -2
- {omlish-0.0.0.dev195.dist-info → omlish-0.0.0.dev197.dist-info}/METADATA +5 -5
- {omlish-0.0.0.dev195.dist-info → omlish-0.0.0.dev197.dist-info}/RECORD +48 -36
- {omlish-0.0.0.dev195.dist-info → omlish-0.0.0.dev197.dist-info}/WHEEL +1 -1
- omlish/io/generators/__init__.py +0 -56
- omlish/io/generators/direct.py +0 -13
- omlish/testing/pytest/marks.py +0 -18
- omlish/testing/pytest/plugins/asyncs.py +0 -162
- /omlish/io/{generators → coro}/consts.py +0 -0
- {omlish-0.0.0.dev195.dist-info → omlish-0.0.0.dev197.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev195.dist-info → omlish-0.0.0.dev197.dist-info}/entry_points.txt +0 -0
- {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,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
|