pytest-asyncio-concurrent 0.1.1__tar.gz

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.
@@ -0,0 +1,22 @@
1
+
2
+ The MIT License (MIT)
3
+
4
+ Copyright (c) 2024 Zane Chen
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ THE SOFTWARE.
@@ -0,0 +1,170 @@
1
+ Metadata-Version: 2.1
2
+ Name: pytest-asyncio-concurrent
3
+ Version: 0.1.1
4
+ Summary: Pytest plugin to execute python async tests concurrently.
5
+ Author-email: Zane Chen <czl970721@gmail.com>
6
+ Maintainer-email: Zane Chen <czl970721@gmail.com>
7
+ License:
8
+ The MIT License (MIT)
9
+
10
+ Copyright (c) 2024 Zane Chen
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in
20
+ all copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
28
+ THE SOFTWARE.
29
+
30
+ Project-URL: Repository, https://github.com/czl9707/pytest-asyncio-concurrent
31
+ Project-URL: Homepage, https://github.com/czl9707/pytest-asyncio-concurrent
32
+ Project-URL: Issues, https://github.com/czl9707/pytest-asyncio-concurrent/issues
33
+ Classifier: Framework :: Pytest
34
+ Classifier: Development Status :: 4 - Beta
35
+ Classifier: Intended Audience :: Developers
36
+ Classifier: Topic :: Software Development :: Testing
37
+ Classifier: Operating System :: OS Independent
38
+ Classifier: Programming Language :: Python
39
+ Classifier: Programming Language :: Python :: 3.8
40
+ Classifier: Programming Language :: Python :: 3.9
41
+ Classifier: Programming Language :: Python :: 3.10
42
+ Classifier: Programming Language :: Python :: 3.11
43
+ Classifier: Programming Language :: Python :: 3.12
44
+ Classifier: Programming Language :: Python :: 3.13
45
+ Classifier: Programming Language :: Python :: 3 :: Only
46
+ Classifier: License :: OSI Approved :: MIT License
47
+ Requires-Python: >=3.8
48
+ Description-Content-Type: text/x-rst
49
+ License-File: LICENSE
50
+ Requires-Dist: pytest>=6.2.0
51
+ Provides-Extra: testing
52
+ Requires-Dist: coverage>=7.6.0; extra == "testing"
53
+
54
+ =========================
55
+ pytest-asyncio-concurrent
56
+ =========================
57
+
58
+ .. image:: https://img.shields.io/pypi/v/pytest-asyncio-concurrent.svg
59
+ :target: https://pypi.org/project/pytest-asyncio-concurrent
60
+ :alt: PyPI version
61
+
62
+ .. image:: https://img.shields.io/pypi/pyversions/pytest-asyncio-concurrent.svg
63
+ :target: https://pypi.org/project/pytest-asyncio-concurrent
64
+ :alt: Python versions
65
+
66
+ .. image:: https://codecov.io/github/czl9707/pytest-asyncio-concurrent/graph/badge.svg?token=ENWHQBWQML
67
+ :target: https://codecov.io/gh/czl9707/pytest-asyncio-concurrent
68
+ :alt: Testing Coverage
69
+
70
+ .. image:: https://github.com/czl9707/pytest-asyncio-concurrent/actions/workflows/main.yml/badge.svg
71
+ :target: https://github.com/czl9707/pytest-asyncio-concurrent/actions/workflows/main.yml
72
+ :alt: See Build Status on GitHub Actions
73
+
74
+
75
+
76
+ System/Integration tests can take a really long time.
77
+
78
+ And ``pytest-asyncio-concurrent`` A pytest plugin is a solution for this by running asynchronous tests in true parallel, enabling faster execution for high I/O or network-bound test suites.
79
+
80
+ Unlike ``pytest-asyncio``, which runs async tests **sequentially**, ``pytest-asyncio-concurrent`` takes advantage of Python's asyncio capabilities to execute tests **concurrently** by specifying **async group**.
81
+
82
+ Note: This plugin would more or less `Break Test Isolation Principle` \(for none function scoped fixture\). Please make sure your tests is ok to run concurrently before you use this plugin.
83
+
84
+
85
+ Key Features
86
+ ------------
87
+
88
+ - Giving the capability to run pytest async functions.
89
+ - Providing granular control over Concurrency
90
+ - Specifying Async Group to control tests that can run together.
91
+ - Specifying Timeout to avoid async tests taking forever. (Under Construction)
92
+ - Compatible with ``pytest-asyncio``.
93
+
94
+ Installation
95
+ ------------
96
+
97
+ You can install "pytest-asyncio-concurrent" via `pip` from `PyPI`::
98
+
99
+ $ pip install pytest-asyncio-concurrent
100
+
101
+
102
+ Usage
103
+ -----
104
+
105
+ Run test Sequentially
106
+
107
+ .. code-block:: python
108
+
109
+ @pytest.mark.asyncio_concurrent
110
+ async def async_test_A():
111
+ res = await wait_for_something_async()
112
+ assert result.is_valid()
113
+
114
+ @pytest.mark.asyncio_concurrent
115
+ async def async_test_B():
116
+ res = await wait_for_something_async()
117
+ assert result.is_valid()
118
+
119
+
120
+ Run tests Concurrently
121
+
122
+ .. code-block:: python
123
+
124
+ # the test below will run by itself
125
+ @pytest.mark.asyncio_concurrent
126
+ async def test_by_itself():
127
+ res = await wait_for_something_async()
128
+ assert result.is_valid()
129
+
130
+ # the two tests below will run concurrently
131
+ @pytest.mark.asyncio_concurrent(group="my_group")
132
+ async def test_groupA():
133
+ res = await wait_for_something_async()
134
+ assert result.is_valid()
135
+
136
+ @pytest.mark.asyncio_concurrent(group="my_group")
137
+ async def test_groupB():
138
+ res = await wait_for_something_async()
139
+ assert result.is_valid()
140
+
141
+
142
+ Parametrized Tests
143
+
144
+ .. code-block:: python
145
+
146
+ # the parametrized tests below will run sequential
147
+ @pytest.mark.asyncio_concurrent
148
+ @pytest.parametrize("p", [0, 1, 2])
149
+ async def test_parametrize_sequential(p):
150
+ res = await wait_for_something_async()
151
+ assert result.is_valid()
152
+
153
+ # the parametrized tests below will run concurrently
154
+ @pytest.mark.asyncio_concurrent(group="my_group")
155
+ @pytest.parametrize("p", [0, 1, 2])
156
+ async def test_parametrize_concurrent():
157
+ res = await wait_for_something_async()
158
+ assert result.is_valid()
159
+
160
+
161
+ Contributing
162
+ ------------
163
+
164
+ Contributions are very welcome. Tests can be run with ``tox``, please ensure
165
+ the coverage at least stays the same before you submit a pull request.
166
+
167
+ License
168
+ -------
169
+
170
+ Distributed under the terms of the ``MIT`` license, "pytest-asyncio-concurrent" is free and open source software
@@ -0,0 +1,117 @@
1
+ =========================
2
+ pytest-asyncio-concurrent
3
+ =========================
4
+
5
+ .. image:: https://img.shields.io/pypi/v/pytest-asyncio-concurrent.svg
6
+ :target: https://pypi.org/project/pytest-asyncio-concurrent
7
+ :alt: PyPI version
8
+
9
+ .. image:: https://img.shields.io/pypi/pyversions/pytest-asyncio-concurrent.svg
10
+ :target: https://pypi.org/project/pytest-asyncio-concurrent
11
+ :alt: Python versions
12
+
13
+ .. image:: https://codecov.io/github/czl9707/pytest-asyncio-concurrent/graph/badge.svg?token=ENWHQBWQML
14
+ :target: https://codecov.io/gh/czl9707/pytest-asyncio-concurrent
15
+ :alt: Testing Coverage
16
+
17
+ .. image:: https://github.com/czl9707/pytest-asyncio-concurrent/actions/workflows/main.yml/badge.svg
18
+ :target: https://github.com/czl9707/pytest-asyncio-concurrent/actions/workflows/main.yml
19
+ :alt: See Build Status on GitHub Actions
20
+
21
+
22
+
23
+ System/Integration tests can take a really long time.
24
+
25
+ And ``pytest-asyncio-concurrent`` A pytest plugin is a solution for this by running asynchronous tests in true parallel, enabling faster execution for high I/O or network-bound test suites.
26
+
27
+ Unlike ``pytest-asyncio``, which runs async tests **sequentially**, ``pytest-asyncio-concurrent`` takes advantage of Python's asyncio capabilities to execute tests **concurrently** by specifying **async group**.
28
+
29
+ Note: This plugin would more or less `Break Test Isolation Principle` \(for none function scoped fixture\). Please make sure your tests is ok to run concurrently before you use this plugin.
30
+
31
+
32
+ Key Features
33
+ ------------
34
+
35
+ - Giving the capability to run pytest async functions.
36
+ - Providing granular control over Concurrency
37
+ - Specifying Async Group to control tests that can run together.
38
+ - Specifying Timeout to avoid async tests taking forever. (Under Construction)
39
+ - Compatible with ``pytest-asyncio``.
40
+
41
+ Installation
42
+ ------------
43
+
44
+ You can install "pytest-asyncio-concurrent" via `pip` from `PyPI`::
45
+
46
+ $ pip install pytest-asyncio-concurrent
47
+
48
+
49
+ Usage
50
+ -----
51
+
52
+ Run test Sequentially
53
+
54
+ .. code-block:: python
55
+
56
+ @pytest.mark.asyncio_concurrent
57
+ async def async_test_A():
58
+ res = await wait_for_something_async()
59
+ assert result.is_valid()
60
+
61
+ @pytest.mark.asyncio_concurrent
62
+ async def async_test_B():
63
+ res = await wait_for_something_async()
64
+ assert result.is_valid()
65
+
66
+
67
+ Run tests Concurrently
68
+
69
+ .. code-block:: python
70
+
71
+ # the test below will run by itself
72
+ @pytest.mark.asyncio_concurrent
73
+ async def test_by_itself():
74
+ res = await wait_for_something_async()
75
+ assert result.is_valid()
76
+
77
+ # the two tests below will run concurrently
78
+ @pytest.mark.asyncio_concurrent(group="my_group")
79
+ async def test_groupA():
80
+ res = await wait_for_something_async()
81
+ assert result.is_valid()
82
+
83
+ @pytest.mark.asyncio_concurrent(group="my_group")
84
+ async def test_groupB():
85
+ res = await wait_for_something_async()
86
+ assert result.is_valid()
87
+
88
+
89
+ Parametrized Tests
90
+
91
+ .. code-block:: python
92
+
93
+ # the parametrized tests below will run sequential
94
+ @pytest.mark.asyncio_concurrent
95
+ @pytest.parametrize("p", [0, 1, 2])
96
+ async def test_parametrize_sequential(p):
97
+ res = await wait_for_something_async()
98
+ assert result.is_valid()
99
+
100
+ # the parametrized tests below will run concurrently
101
+ @pytest.mark.asyncio_concurrent(group="my_group")
102
+ @pytest.parametrize("p", [0, 1, 2])
103
+ async def test_parametrize_concurrent():
104
+ res = await wait_for_something_async()
105
+ assert result.is_valid()
106
+
107
+
108
+ Contributing
109
+ ------------
110
+
111
+ Contributions are very welcome. Tests can be run with ``tox``, please ensure
112
+ the coverage at least stays the same before you submit a pull request.
113
+
114
+ License
115
+ -------
116
+
117
+ Distributed under the terms of the ``MIT`` license, "pytest-asyncio-concurrent" is free and open source software
@@ -0,0 +1,116 @@
1
+ [build-system]
2
+ requires = [
3
+ "setuptools>=61.0",
4
+ ]
5
+ build-backend = "setuptools.build_meta"
6
+
7
+ [project]
8
+ name = "pytest-asyncio-concurrent"
9
+ description = "Pytest plugin to execute python async tests concurrently."
10
+ version = "0.1.1"
11
+ readme = "README.rst"
12
+ requires-python = ">=3.8"
13
+ authors = [
14
+ { name = "Zane Chen", email = "czl970721@gmail.com" },
15
+ ]
16
+ maintainers = [
17
+ { name = "Zane Chen", email = "czl970721@gmail.com" },
18
+ ]
19
+ license = {file = "LICENSE"}
20
+ classifiers = [
21
+ "Framework :: Pytest",
22
+ "Development Status :: 4 - Beta",
23
+ "Intended Audience :: Developers",
24
+ "Topic :: Software Development :: Testing",
25
+ "Operating System :: OS Independent",
26
+ "Programming Language :: Python",
27
+ "Programming Language :: Python :: 3.8",
28
+ "Programming Language :: Python :: 3.9",
29
+ "Programming Language :: Python :: 3.10",
30
+ "Programming Language :: Python :: 3.11",
31
+ "Programming Language :: Python :: 3.12",
32
+ "Programming Language :: Python :: 3.13",
33
+ "Programming Language :: Python :: 3 :: Only",
34
+ "License :: OSI Approved :: MIT License",
35
+ ]
36
+ dependencies = [
37
+ "pytest>=6.2.0",
38
+ ]
39
+ optional-dependencies.testing = [
40
+ "coverage>=7.6.0",
41
+ ]
42
+ [project.urls]
43
+ Repository = "https://github.com/czl9707/pytest-asyncio-concurrent"
44
+ Homepage = "https://github.com/czl9707/pytest-asyncio-concurrent"
45
+ Issues = "https://github.com/czl9707/pytest-asyncio-concurrent/issues"
46
+
47
+ [project.entry-points.pytest11]
48
+ asyncio-concurrent = "pytest_asyncio_concurrent.plugin"
49
+
50
+ [tool.setuptools]
51
+ packages = [
52
+ "pytest_asyncio_concurrent",
53
+ ]
54
+ include-package-data = true
55
+ license-files = [
56
+ "LICENSE",
57
+ ]
58
+
59
+ [tool.black]
60
+ line-length = 100
61
+
62
+ [tool.flake8]
63
+ max-line-length = 100
64
+
65
+ [tool.coverage.run]
66
+ source = [
67
+ "pytest_asyncio_concurrent",
68
+ ]
69
+ branch = true
70
+ data_file = "coverage/coverage"
71
+ omit = [
72
+ "*/_version.py",
73
+ ]
74
+ parallel = true
75
+ concurrency = [
76
+ "multiprocessing"
77
+ ]
78
+
79
+ [tool.coverage.report]
80
+ show_missing = true
81
+
82
+ [tool.bumpversion]
83
+ current_version = "0.1.1"
84
+ parse = """(?x)
85
+ (?P<major>0|[1-9]\\d*)\\.
86
+ (?P<minor>0|[1-9]\\d*)\\.
87
+ (?P<patch>0|[1-9]\\d*)
88
+ (?:
89
+ -prerelease-(?P<pre>0|[1-9]\\d*)
90
+ )?
91
+ """
92
+ serialize = [
93
+ "{major}.{minor}.{patch}-prerelease-{pre}",
94
+ "{major}.{minor}.{patch}",
95
+ ]
96
+ search = "{current_version}"
97
+ replace = "{new_version}"
98
+ regex = false
99
+ ignore_missing_version = false
100
+ ignore_missing_files = false
101
+ tag = true
102
+ sign_tags = false
103
+ tag_name = "v{new_version}"
104
+ tag_message = "Bump version: {current_version} → {new_version}"
105
+ allow_dirty = false
106
+ commit = true
107
+ message = "Bump version: {current_version} → {new_version}"
108
+ commit_args = ""
109
+ setup_hooks = []
110
+ pre_commit_hooks = []
111
+ post_commit_hooks = []
112
+
113
+ [[tool.bumpversion.files]]
114
+ filename = "pyproject.toml"
115
+ search = "version = \"{current_version}\""
116
+ replace = "version = \"{new_version}\""
@@ -0,0 +1,8 @@
1
+ """The main point for importing pytest-asyncio-concurrent items."""
2
+
3
+ from typing import List
4
+ from .plugin import AsyncioConcurrentGroup
5
+
6
+ __all__: List[str] = [
7
+ AsyncioConcurrentGroup.__name__,
8
+ ]
@@ -0,0 +1,270 @@
1
+ import asyncio
2
+ from typing import Any, Callable, Generator, List, Optional, Coroutine, Dict, cast
3
+ import uuid
4
+
5
+ import pytest
6
+ from _pytest import scope, timing, outcomes, runner
7
+ from pytest import (
8
+ CallInfo,
9
+ ExceptionInfo,
10
+ FixtureDef,
11
+ Item,
12
+ Session,
13
+ Config,
14
+ Function,
15
+ Mark,
16
+ TestReport,
17
+ )
18
+
19
+
20
+ # =========================== # Config & Collection # =========================== #
21
+ def pytest_configure(config: Config) -> None:
22
+ config.addinivalue_line(
23
+ "markers",
24
+ "asyncio_concurrent(group, timeout): " "mark the tests to run concurrently",
25
+ )
26
+
27
+
28
+ @pytest.hookimpl(specname="pytest_runtestloop", wrapper=True)
29
+ def pytest_runtestloop_wrap_items_by_group(session: Session) -> Generator[None, Any, Any]:
30
+ """
31
+ Group items with same asyncio concurrent group together,
32
+ so they can be executed together in outer loop.
33
+ """
34
+ asycio_concurrent_groups: Dict[str, List[Function]] = {}
35
+ items = session.items
36
+
37
+ for item in items:
38
+ if _get_asyncio_concurrent_mark(item) is None:
39
+ continue
40
+
41
+ concurrent_group_name = _get_asyncio_concurrent_group(item)
42
+ if concurrent_group_name not in asycio_concurrent_groups:
43
+ asycio_concurrent_groups[concurrent_group_name] = []
44
+ asycio_concurrent_groups[concurrent_group_name].append(cast(Function, item))
45
+
46
+ for asyncio_items in asycio_concurrent_groups.values():
47
+ for item in asyncio_items:
48
+ items.remove(item)
49
+
50
+ for group_name, asyncio_items in asycio_concurrent_groups.items():
51
+ items.append(group_asyncio_concurrent_function(group_name, asyncio_items))
52
+
53
+ result = yield
54
+
55
+ groups = [item for item in items if isinstance(item, AsyncioConcurrentGroup)]
56
+ for group in groups:
57
+ items.remove(group)
58
+ for item in group._pytest_asyncio_concurrent_children:
59
+ items.append(item)
60
+
61
+ return result
62
+
63
+
64
+ class AsyncioConcurrentGroup(Function):
65
+ _pytest_asyncio_concurrent_children: List[Function] = []
66
+
67
+
68
+ def group_asyncio_concurrent_function(
69
+ group_name: str, children: List[Function]
70
+ ) -> AsyncioConcurrentGroup:
71
+ parent = None
72
+ for childFunc in children:
73
+ p_it = childFunc.iter_parents()
74
+ next(p_it)
75
+ func_parent = next(p_it)
76
+
77
+ if not parent:
78
+ parent = func_parent
79
+ elif parent is not func_parent:
80
+ raise Exception("test case within same group should have same parent.")
81
+
82
+ _rewrite_function_scoped_fixture(childFunc)
83
+
84
+ g_function = AsyncioConcurrentGroup.from_parent(
85
+ parent,
86
+ name=f"ayncio_concurrent_test_group[{group_name}]",
87
+ callobj=lambda: None,
88
+ )
89
+
90
+ g_function._pytest_asyncio_concurrent_children = children
91
+ return g_function
92
+
93
+
94
+ def _rewrite_function_scoped_fixture(item: Function):
95
+ for name, fixturedefs in item._request._arg2fixturedefs.items():
96
+ if hasattr(item, "callspec") and name in item.callspec.params.keys():
97
+ continue
98
+
99
+ if fixturedefs[-1]._scope != scope.Scope.Function:
100
+ continue
101
+
102
+ new_fixdef = FixtureDef(
103
+ config=item.config,
104
+ baseid=fixturedefs[-1].baseid,
105
+ argname=fixturedefs[-1].argname,
106
+ func=fixturedefs[-1].func,
107
+ scope=fixturedefs[-1]._scope,
108
+ params=fixturedefs[-1].params,
109
+ ids=fixturedefs[-1].ids,
110
+ _ispytest=True,
111
+ )
112
+ fixturedefs = list(fixturedefs[0:-1]) + [new_fixdef]
113
+ item._request._arg2fixturedefs[name] = fixturedefs
114
+
115
+
116
+ # =========================== # function call & setup & teardown #===========================#
117
+
118
+
119
+ @pytest.hookimpl(specname="pytest_runtest_setup", wrapper=True)
120
+ def pytest_runtest_setup_group_children(item: Item) -> Generator[None, None, None]:
121
+ result = yield
122
+
123
+ if not isinstance(item, AsyncioConcurrentGroup):
124
+ return result
125
+
126
+ for childFunc in item._pytest_asyncio_concurrent_children:
127
+ call = CallInfo.from_call(_pytest_simple_setup(childFunc), "setup")
128
+ report: TestReport = childFunc.ihook.pytest_runtest_makereport(item=childFunc, call=call)
129
+ childFunc.ihook.pytest_runtest_logreport(report=report)
130
+
131
+ return result
132
+
133
+
134
+ def _pytest_simple_setup(item: Item) -> Callable[[], None]:
135
+ def inner() -> None:
136
+ item.session._setupstate.stack[item] = ([item.teardown], None)
137
+ item.setup()
138
+
139
+ return inner
140
+
141
+
142
+ @pytest.hookimpl(specname="pytest_pyfunc_call", wrapper=True)
143
+ def pytest_pyfunc_call_handle_group(pyfuncitem: Function) -> Generator[None, Any, Any]:
144
+ result = yield
145
+ if not isinstance(pyfuncitem, AsyncioConcurrentGroup):
146
+ return result
147
+
148
+ coros: List[Coroutine] = []
149
+ loop = asyncio.get_event_loop()
150
+
151
+ for childFunc in pyfuncitem._pytest_asyncio_concurrent_children:
152
+ coros.append(_async_callinfo_from_call(_pytest_function_call_async(childFunc)))
153
+
154
+ call_result = loop.run_until_complete(asyncio.gather(*coros))
155
+
156
+ for childFunc, call in zip(pyfuncitem._pytest_asyncio_concurrent_children, call_result):
157
+ report: TestReport = childFunc.ihook.pytest_runtest_makereport(item=childFunc, call=call)
158
+ childFunc.ihook.pytest_runtest_logreport(report=report)
159
+
160
+ return result
161
+
162
+
163
+ def _pytest_function_call_async(item: Function) -> Callable[[], Coroutine]:
164
+ async def inner() -> Any:
165
+ testfunction = item.obj
166
+ testargs = {arg: item.funcargs[arg] for arg in item._fixtureinfo.argnames}
167
+ return await testfunction(**testargs)
168
+
169
+ return inner
170
+
171
+
172
+ # referencing CallInfo.from_call
173
+ async def _async_callinfo_from_call(func: Callable[[], Coroutine]) -> CallInfo:
174
+ excinfo = None
175
+ start = timing.time()
176
+ precise_start = timing.perf_counter()
177
+ try:
178
+ result = await func()
179
+ except BaseException:
180
+ excinfo = ExceptionInfo.from_current()
181
+ if isinstance(excinfo.value, outcomes.Exit):
182
+ raise
183
+ result = None
184
+
185
+ precise_stop = timing.perf_counter()
186
+ duration = precise_stop - precise_start
187
+ stop = timing.time()
188
+
189
+ callInfo: CallInfo = CallInfo(
190
+ start=start,
191
+ stop=stop,
192
+ duration=duration,
193
+ when="call",
194
+ result=result,
195
+ excinfo=excinfo,
196
+ _ispytest=True,
197
+ )
198
+
199
+ return callInfo
200
+
201
+
202
+ @pytest.hookimpl(specname="pytest_runtest_teardown", wrapper=True)
203
+ def pytest_runtest_teardown_group_children(
204
+ item: Item, nextitem: Optional[Item]
205
+ ) -> Generator[None, None, None]:
206
+ if not isinstance(item, AsyncioConcurrentGroup):
207
+ return (yield)
208
+
209
+ for childFunc in item._pytest_asyncio_concurrent_children:
210
+ call = CallInfo.from_call(_pytest_simple_teardown(childFunc), "teardown")
211
+ report: TestReport = childFunc.ihook.pytest_runtest_makereport(item=childFunc, call=call)
212
+ childFunc.ihook.pytest_runtest_logreport(report=report)
213
+
214
+ return (yield)
215
+
216
+
217
+ def _pytest_simple_teardown(item: Item) -> Callable[[], None]:
218
+ def inner() -> None:
219
+ finalizers, _ = item.session._setupstate.stack.pop(item)
220
+ these_exceptions = []
221
+ while finalizers:
222
+ fin = finalizers.pop()
223
+ try:
224
+ fin()
225
+ except Exception as e:
226
+ these_exceptions.append(e)
227
+
228
+ if len(these_exceptions) == 1:
229
+ raise these_exceptions[0]
230
+ elif these_exceptions:
231
+ msg = f"Errors during tearing down {item}"
232
+ raise BaseExceptionGroup(msg, these_exceptions[::-1])
233
+
234
+ return inner
235
+
236
+
237
+ # =========================== # reporting #===========================#
238
+
239
+
240
+ @pytest.hookimpl(specname="pytest_runtest_protocol", tryfirst=True)
241
+ def pytest_runtest_protocol_skip_logging_for_group(
242
+ item: Item, nextitem: Optional[Item]
243
+ ) -> Optional[bool]:
244
+ if not isinstance(item, AsyncioConcurrentGroup):
245
+ return None
246
+
247
+ for childFunc in item._pytest_asyncio_concurrent_children:
248
+ childFunc.ihook.pytest_runtest_logstart(
249
+ nodeid=childFunc.nodeid, location=childFunc.location
250
+ )
251
+
252
+ runner.runtestprotocol(item, nextitem=nextitem, log=False) # disable logging for group function
253
+
254
+ for childFunc in item._pytest_asyncio_concurrent_children:
255
+ childFunc.ihook.pytest_runtest_logfinish(
256
+ nodeid=childFunc.nodeid, location=childFunc.location
257
+ )
258
+
259
+ return True
260
+
261
+
262
+ def _get_asyncio_concurrent_mark(item: Item) -> Optional[Mark]:
263
+ return item.get_closest_marker("asyncio_concurrent")
264
+
265
+
266
+ def _get_asyncio_concurrent_group(item: Item) -> str:
267
+ marker = item.get_closest_marker("asyncio_concurrent")
268
+ assert marker is not None
269
+
270
+ return marker.kwargs.get("group", f"anonymous_[{uuid.uuid4()}]")
@@ -0,0 +1,170 @@
1
+ Metadata-Version: 2.1
2
+ Name: pytest-asyncio-concurrent
3
+ Version: 0.1.1
4
+ Summary: Pytest plugin to execute python async tests concurrently.
5
+ Author-email: Zane Chen <czl970721@gmail.com>
6
+ Maintainer-email: Zane Chen <czl970721@gmail.com>
7
+ License:
8
+ The MIT License (MIT)
9
+
10
+ Copyright (c) 2024 Zane Chen
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in
20
+ all copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
28
+ THE SOFTWARE.
29
+
30
+ Project-URL: Repository, https://github.com/czl9707/pytest-asyncio-concurrent
31
+ Project-URL: Homepage, https://github.com/czl9707/pytest-asyncio-concurrent
32
+ Project-URL: Issues, https://github.com/czl9707/pytest-asyncio-concurrent/issues
33
+ Classifier: Framework :: Pytest
34
+ Classifier: Development Status :: 4 - Beta
35
+ Classifier: Intended Audience :: Developers
36
+ Classifier: Topic :: Software Development :: Testing
37
+ Classifier: Operating System :: OS Independent
38
+ Classifier: Programming Language :: Python
39
+ Classifier: Programming Language :: Python :: 3.8
40
+ Classifier: Programming Language :: Python :: 3.9
41
+ Classifier: Programming Language :: Python :: 3.10
42
+ Classifier: Programming Language :: Python :: 3.11
43
+ Classifier: Programming Language :: Python :: 3.12
44
+ Classifier: Programming Language :: Python :: 3.13
45
+ Classifier: Programming Language :: Python :: 3 :: Only
46
+ Classifier: License :: OSI Approved :: MIT License
47
+ Requires-Python: >=3.8
48
+ Description-Content-Type: text/x-rst
49
+ License-File: LICENSE
50
+ Requires-Dist: pytest>=6.2.0
51
+ Provides-Extra: testing
52
+ Requires-Dist: coverage>=7.6.0; extra == "testing"
53
+
54
+ =========================
55
+ pytest-asyncio-concurrent
56
+ =========================
57
+
58
+ .. image:: https://img.shields.io/pypi/v/pytest-asyncio-concurrent.svg
59
+ :target: https://pypi.org/project/pytest-asyncio-concurrent
60
+ :alt: PyPI version
61
+
62
+ .. image:: https://img.shields.io/pypi/pyversions/pytest-asyncio-concurrent.svg
63
+ :target: https://pypi.org/project/pytest-asyncio-concurrent
64
+ :alt: Python versions
65
+
66
+ .. image:: https://codecov.io/github/czl9707/pytest-asyncio-concurrent/graph/badge.svg?token=ENWHQBWQML
67
+ :target: https://codecov.io/gh/czl9707/pytest-asyncio-concurrent
68
+ :alt: Testing Coverage
69
+
70
+ .. image:: https://github.com/czl9707/pytest-asyncio-concurrent/actions/workflows/main.yml/badge.svg
71
+ :target: https://github.com/czl9707/pytest-asyncio-concurrent/actions/workflows/main.yml
72
+ :alt: See Build Status on GitHub Actions
73
+
74
+
75
+
76
+ System/Integration tests can take a really long time.
77
+
78
+ And ``pytest-asyncio-concurrent`` A pytest plugin is a solution for this by running asynchronous tests in true parallel, enabling faster execution for high I/O or network-bound test suites.
79
+
80
+ Unlike ``pytest-asyncio``, which runs async tests **sequentially**, ``pytest-asyncio-concurrent`` takes advantage of Python's asyncio capabilities to execute tests **concurrently** by specifying **async group**.
81
+
82
+ Note: This plugin would more or less `Break Test Isolation Principle` \(for none function scoped fixture\). Please make sure your tests is ok to run concurrently before you use this plugin.
83
+
84
+
85
+ Key Features
86
+ ------------
87
+
88
+ - Giving the capability to run pytest async functions.
89
+ - Providing granular control over Concurrency
90
+ - Specifying Async Group to control tests that can run together.
91
+ - Specifying Timeout to avoid async tests taking forever. (Under Construction)
92
+ - Compatible with ``pytest-asyncio``.
93
+
94
+ Installation
95
+ ------------
96
+
97
+ You can install "pytest-asyncio-concurrent" via `pip` from `PyPI`::
98
+
99
+ $ pip install pytest-asyncio-concurrent
100
+
101
+
102
+ Usage
103
+ -----
104
+
105
+ Run test Sequentially
106
+
107
+ .. code-block:: python
108
+
109
+ @pytest.mark.asyncio_concurrent
110
+ async def async_test_A():
111
+ res = await wait_for_something_async()
112
+ assert result.is_valid()
113
+
114
+ @pytest.mark.asyncio_concurrent
115
+ async def async_test_B():
116
+ res = await wait_for_something_async()
117
+ assert result.is_valid()
118
+
119
+
120
+ Run tests Concurrently
121
+
122
+ .. code-block:: python
123
+
124
+ # the test below will run by itself
125
+ @pytest.mark.asyncio_concurrent
126
+ async def test_by_itself():
127
+ res = await wait_for_something_async()
128
+ assert result.is_valid()
129
+
130
+ # the two tests below will run concurrently
131
+ @pytest.mark.asyncio_concurrent(group="my_group")
132
+ async def test_groupA():
133
+ res = await wait_for_something_async()
134
+ assert result.is_valid()
135
+
136
+ @pytest.mark.asyncio_concurrent(group="my_group")
137
+ async def test_groupB():
138
+ res = await wait_for_something_async()
139
+ assert result.is_valid()
140
+
141
+
142
+ Parametrized Tests
143
+
144
+ .. code-block:: python
145
+
146
+ # the parametrized tests below will run sequential
147
+ @pytest.mark.asyncio_concurrent
148
+ @pytest.parametrize("p", [0, 1, 2])
149
+ async def test_parametrize_sequential(p):
150
+ res = await wait_for_something_async()
151
+ assert result.is_valid()
152
+
153
+ # the parametrized tests below will run concurrently
154
+ @pytest.mark.asyncio_concurrent(group="my_group")
155
+ @pytest.parametrize("p", [0, 1, 2])
156
+ async def test_parametrize_concurrent():
157
+ res = await wait_for_something_async()
158
+ assert result.is_valid()
159
+
160
+
161
+ Contributing
162
+ ------------
163
+
164
+ Contributions are very welcome. Tests can be run with ``tox``, please ensure
165
+ the coverage at least stays the same before you submit a pull request.
166
+
167
+ License
168
+ -------
169
+
170
+ Distributed under the terms of the ``MIT`` license, "pytest-asyncio-concurrent" is free and open source software
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.rst
3
+ pyproject.toml
4
+ pytest_asyncio_concurrent/__init__.py
5
+ pytest_asyncio_concurrent/plugin.py
6
+ pytest_asyncio_concurrent.egg-info/PKG-INFO
7
+ pytest_asyncio_concurrent.egg-info/SOURCES.txt
8
+ pytest_asyncio_concurrent.egg-info/dependency_links.txt
9
+ pytest_asyncio_concurrent.egg-info/entry_points.txt
10
+ pytest_asyncio_concurrent.egg-info/requires.txt
11
+ pytest_asyncio_concurrent.egg-info/top_level.txt
12
+ tests/test_fixture.py
13
+ tests/test_grouping.py
@@ -0,0 +1,2 @@
1
+ [pytest11]
2
+ asyncio-concurrent = pytest_asyncio_concurrent.plugin
@@ -0,0 +1,4 @@
1
+ pytest>=6.2.0
2
+
3
+ [testing]
4
+ coverage>=7.6.0
@@ -0,0 +1 @@
1
+ pytest_asyncio_concurrent
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,160 @@
1
+ from textwrap import dedent
2
+ import pytest
3
+
4
+
5
+ def test_fixture_handling(pytester: pytest.Pytester):
6
+ """Make sure that pytest accepts our fixture."""
7
+
8
+ pytester.makeconftest(
9
+ dedent(
10
+ """\
11
+ import pytest
12
+
13
+ @pytest.fixture
14
+ def fixture_a():
15
+ yield 1
16
+
17
+
18
+ @pytest.fixture
19
+ def fixture_b():
20
+ yield 2
21
+ """
22
+ )
23
+ )
24
+
25
+ pytester.makepyfile(
26
+ dedent(
27
+ """\
28
+ import asyncio
29
+ import pytest
30
+
31
+ @pytest.mark.asyncio_concurrent
32
+ async def test_fixture_multi(fixture_a, fixture_b):
33
+ await asyncio.sleep(1)
34
+ assert fixture_a == 1
35
+ assert fixture_b == 2
36
+ """
37
+ )
38
+ )
39
+
40
+ result = pytester.runpytest()
41
+
42
+ result.assert_outcomes(passed=1)
43
+
44
+
45
+ def test_fixture_scopes(pytester: pytest.Pytester):
46
+ """Make sure that pytest accepts our fixture."""
47
+
48
+ pytester.makeconftest(
49
+ dedent(
50
+ """\
51
+ import pytest
52
+
53
+ @pytest.fixture(scope="function")
54
+ def fixture_function():
55
+ yield "fixture_function"
56
+
57
+ @pytest.fixture(scope="class")
58
+ def fixture_class():
59
+ yield "fixture_class"
60
+
61
+ @pytest.fixture(scope="module")
62
+ def fixture_module():
63
+ yield "fixture_module"
64
+
65
+ @pytest.fixture(scope="session")
66
+ def fixture_session():
67
+ yield "fixture_session"
68
+ """
69
+ )
70
+ )
71
+
72
+ pytester.makepyfile(
73
+ dedent(
74
+ """\
75
+ import asyncio
76
+ import pytest
77
+
78
+ @pytest.mark.asyncio_concurrent
79
+ async def test_fixture_multi(
80
+ fixture_function,
81
+ fixture_class,
82
+ fixture_module,
83
+ fixture_session
84
+ ):
85
+ await asyncio.sleep(1)
86
+ assert fixture_function == "fixture_function"
87
+ assert fixture_class == "fixture_class"
88
+ assert fixture_module == "fixture_module"
89
+ assert fixture_session == "fixture_session"
90
+ """
91
+ )
92
+ )
93
+
94
+ result = pytester.runpytest()
95
+
96
+ result.assert_outcomes(passed=1)
97
+
98
+
99
+ def test_fixture_teardown(pytester: pytest.Pytester):
100
+ """Make sure that pytest accepts our fixture."""
101
+
102
+ pytester.makeconftest(
103
+ dedent(
104
+ """\
105
+ import pytest
106
+
107
+ @pytest.fixture(scope="function")
108
+ def fixture_function():
109
+ yield []
110
+
111
+ @pytest.fixture(scope="module")
112
+ def fixture_module():
113
+ yield []
114
+ """
115
+ )
116
+ )
117
+
118
+ pytester.makepyfile(
119
+ testA=dedent(
120
+ """\
121
+ import asyncio
122
+ import pytest
123
+
124
+ @pytest.mark.asyncio_concurrent(group="any")
125
+ @pytest.mark.parametrize("p", [1, 2, 3])
126
+ async def test_fixture_multi(fixture_function, fixture_module, p):
127
+ await asyncio.sleep(p)
128
+
129
+ fixture_module.append(p)
130
+ fixture_function.append(p)
131
+
132
+ assert len(fixture_function) == 1
133
+ assert len(fixture_module) == p
134
+ """
135
+ )
136
+ )
137
+
138
+ pytester.makepyfile(
139
+ testB=dedent(
140
+ """\
141
+ import asyncio
142
+ import pytest
143
+
144
+ @pytest.mark.asyncio_concurrent
145
+ @pytest.mark.parametrize("p", [1, 2, 3])
146
+ async def test_fixture_multi(fixture_function, fixture_module, p):
147
+ await asyncio.sleep(p)
148
+
149
+ fixture_module.append(p)
150
+ fixture_function.append(p)
151
+
152
+ assert len(fixture_function) == 1
153
+ assert len(fixture_module) == p
154
+ """
155
+ )
156
+ )
157
+
158
+ result = pytester.runpytest("testA.py", "testB.py")
159
+
160
+ result.assert_outcomes(passed=6)
@@ -0,0 +1,144 @@
1
+ from textwrap import dedent
2
+ import pytest
3
+
4
+
5
+ def test_groups_different(pytester: pytest.Pytester):
6
+ """Make sure group with different group exceuted seperately."""
7
+
8
+ pytester.makepyfile(
9
+ dedent(
10
+ """\
11
+ import asyncio
12
+ import pytest
13
+
14
+ @pytest.mark.asyncio_concurrent(group="A")
15
+ async def test_group_A():
16
+ await asyncio.sleep(2)
17
+ assert 1 == 1
18
+
19
+ @pytest.mark.asyncio_concurrent(group="B")
20
+ async def test_group_B():
21
+ await asyncio.sleep(1)
22
+ assert 1 == 1
23
+ """
24
+ )
25
+ )
26
+
27
+ result = pytester.runpytest()
28
+
29
+ assert result.duration >= 3
30
+ result.assert_outcomes(passed=2)
31
+
32
+
33
+ def test_groups_anonymous(pytester: pytest.Pytester):
34
+ """Make sure tests without group specified treated as different group"""
35
+
36
+ pytester.makepyfile(
37
+ dedent(
38
+ """\
39
+ import asyncio
40
+ import pytest
41
+
42
+ @pytest.mark.asyncio_concurrent
43
+ async def test_group_A():
44
+ await asyncio.sleep(2)
45
+ assert 1 == 1
46
+
47
+ @pytest.mark.asyncio_concurrent
48
+ async def test_group_B():
49
+ await asyncio.sleep(1)
50
+ assert 1 == 1
51
+ """
52
+ )
53
+ )
54
+
55
+ result = pytester.runpytest()
56
+
57
+ assert result.duration >= 3
58
+ result.assert_outcomes(passed=2)
59
+
60
+
61
+ def test_groups_same(pytester: pytest.Pytester):
62
+ """Make sure group with same group exceuted together."""
63
+
64
+ pytester.makepyfile(
65
+ dedent(
66
+ """\
67
+ import asyncio
68
+ import pytest
69
+
70
+ @pytest.mark.asyncio_concurrent(group="A")
71
+ async def test_group_anonymous_A():
72
+ await asyncio.sleep(2)
73
+ assert 1 == 1
74
+
75
+ @pytest.mark.asyncio_concurrent(group="A")
76
+ async def test_group_anonymous_B():
77
+ await asyncio.sleep(1)
78
+ assert 1 == 1
79
+ """
80
+ )
81
+ )
82
+
83
+ result = pytester.runpytest()
84
+
85
+ assert result.duration < 3
86
+ result.assert_outcomes(passed=2)
87
+
88
+
89
+ def test_parametrize_without_group(pytester: pytest.Pytester):
90
+ """Make sure parametrized tests without group specified treated as different group"""
91
+
92
+ pytester.makepyfile(
93
+ dedent(
94
+ """\
95
+ import asyncio
96
+ import pytest
97
+
98
+ g = 0
99
+
100
+ @pytest.mark.parametrize("p", [0, 1, 2])
101
+ @pytest.mark.asyncio_concurrent
102
+ async def test_parametrize_no_group(p):
103
+ global g
104
+ await asyncio.sleep(p)
105
+
106
+ assert g == p
107
+ g += 1
108
+ """
109
+ )
110
+ )
111
+
112
+ result = pytester.runpytest()
113
+
114
+ result.assert_outcomes(passed=3)
115
+ assert result.duration >= 3
116
+
117
+
118
+ def test_parametrize_with_group(pytester: pytest.Pytester):
119
+ """Make sure parametrized tests with group specified executed together"""
120
+
121
+ pytester.makepyfile(
122
+ dedent(
123
+ """\
124
+ import asyncio
125
+ import pytest
126
+
127
+ g = 0
128
+
129
+ @pytest.mark.parametrize("p", [0, 1, 2])
130
+ @pytest.mark.asyncio_concurrent(group="any")
131
+ async def test_parametrize_with_group(p):
132
+ global g
133
+ await asyncio.sleep(p)
134
+
135
+ assert g == p
136
+ g += 1
137
+ """
138
+ )
139
+ )
140
+
141
+ result = pytester.runpytest()
142
+
143
+ result.assert_outcomes(passed=3)
144
+ assert result.duration < 3