asyncio-extensions 0.0.2__tar.gz → 0.0.3__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.
- {asyncio_extensions-0.0.2 → asyncio_extensions-0.0.3}/.github/dependabot.yml +0 -4
- asyncio_extensions-0.0.3/.github/workflows/release.yml +57 -0
- {asyncio_extensions-0.0.2 → asyncio_extensions-0.0.3}/.github/workflows/tests.yml +15 -3
- asyncio_extensions-0.0.3/.gitignore +5 -0
- asyncio_extensions-0.0.3/.pre-commit-config.yaml +36 -0
- asyncio_extensions-0.0.3/CHANGELOG.md +43 -0
- asyncio_extensions-0.0.3/PKG-INFO +133 -0
- asyncio_extensions-0.0.3/README.md +104 -0
- asyncio_extensions-0.0.3/codecov.yml +13 -0
- {asyncio_extensions-0.0.2 → asyncio_extensions-0.0.3}/pyproject.toml +12 -2
- asyncio_extensions-0.0.3/src/asyncio_extensions/__init__.py +19 -0
- asyncio_extensions-0.0.3/src/asyncio_extensions/_compat.py +10 -0
- asyncio_extensions-0.0.3/src/asyncio_extensions/taskgroups.py +93 -0
- asyncio_extensions-0.0.3/src/asyncio_extensions/utils.py +49 -0
- asyncio_extensions-0.0.3/tests/test_taskgroups.py +109 -0
- {asyncio_extensions-0.0.2 → asyncio_extensions-0.0.3}/tests/test_utils.py +13 -2
- asyncio_extensions-0.0.2/.github/workflows/release.yml +0 -33
- asyncio_extensions-0.0.2/.gitignore +0 -2
- asyncio_extensions-0.0.2/PKG-INFO +0 -47
- asyncio_extensions-0.0.2/README.md +0 -21
- asyncio_extensions-0.0.2/src/asyncio_extensions/__init__.py +0 -12
- asyncio_extensions-0.0.2/src/asyncio_extensions/taskgroups.py +0 -38
- asyncio_extensions-0.0.2/src/asyncio_extensions/utils.py +0 -27
- asyncio_extensions-0.0.2/tests/__init__.py +0 -0
- asyncio_extensions-0.0.2/tests/test_taskgroups.py +0 -54
- {asyncio_extensions-0.0.2 → asyncio_extensions-0.0.3}/LICENSE.txt +0 -0
- {asyncio_extensions-0.0.2 → asyncio_extensions-0.0.3}/src/asyncio_extensions/py.typed +0 -0
- /asyncio_extensions-0.0.2/tests/utils.py → /asyncio_extensions-0.0.3/tests/__init__.py +0 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
name: Build & Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
name: Build distribution 📦
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v6
|
|
17
|
+
with:
|
|
18
|
+
persist-credentials: false
|
|
19
|
+
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
uses: actions/setup-python@v6
|
|
22
|
+
with:
|
|
23
|
+
python-version: "3.x"
|
|
24
|
+
|
|
25
|
+
- name: Install pypa/build
|
|
26
|
+
run: |
|
|
27
|
+
python -m pip install --upgrade pip build --user
|
|
28
|
+
|
|
29
|
+
- name: Build a binary wheel and a source tarball
|
|
30
|
+
run: python -m build
|
|
31
|
+
|
|
32
|
+
- name: Store the distribution packages
|
|
33
|
+
uses: actions/upload-artifact@v6
|
|
34
|
+
with:
|
|
35
|
+
name: python-package-distributions
|
|
36
|
+
path: dist/
|
|
37
|
+
|
|
38
|
+
publish-to-pypi:
|
|
39
|
+
name: Publish Python 🐍 distribution 📦 to PyPI
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
needs:
|
|
42
|
+
- build
|
|
43
|
+
environment:
|
|
44
|
+
name: release
|
|
45
|
+
url: https://pypi.org/p/asyncio-extensions
|
|
46
|
+
permissions:
|
|
47
|
+
id-token: write
|
|
48
|
+
|
|
49
|
+
steps:
|
|
50
|
+
- name: Download all the dists
|
|
51
|
+
uses: actions/download-artifact@v7
|
|
52
|
+
with:
|
|
53
|
+
name: python-package-distributions
|
|
54
|
+
path: dist/
|
|
55
|
+
|
|
56
|
+
- name: Publish distribution 📦 to PyPI
|
|
57
|
+
uses: pypa/gh-action-pypi-publish@v1.13.0
|
|
@@ -27,7 +27,7 @@ jobs:
|
|
|
27
27
|
python-version: "3.13"
|
|
28
28
|
|
|
29
29
|
- name: Setup Hatch
|
|
30
|
-
run: pipx
|
|
30
|
+
run: pipx install --preinstall "virtualenv<21" hatch
|
|
31
31
|
|
|
32
32
|
- name: Run static analysis
|
|
33
33
|
run: hatch fmt --check
|
|
@@ -51,8 +51,20 @@ jobs:
|
|
|
51
51
|
python-version: ${{ matrix.python-version }}
|
|
52
52
|
|
|
53
53
|
- name: Setup Hatch
|
|
54
|
-
run: pipx
|
|
54
|
+
run: pipx install --preinstall "virtualenv<21" hatch
|
|
55
55
|
|
|
56
56
|
- name: Run tests
|
|
57
57
|
run: |
|
|
58
|
-
hatch test -i py=${{ matrix.python-version }} --cover
|
|
58
|
+
hatch test -i py=${{ matrix.python-version }} --cover --junitxml=junit.xml
|
|
59
|
+
|
|
60
|
+
- name: Upload results to Codecov
|
|
61
|
+
uses: codecov/codecov-action@v5
|
|
62
|
+
with:
|
|
63
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
64
|
+
|
|
65
|
+
- name: Upload test results to Codecov
|
|
66
|
+
if: ${{ !cancelled() }}
|
|
67
|
+
uses: codecov/codecov-action@v5
|
|
68
|
+
with:
|
|
69
|
+
report_type: test_results
|
|
70
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
+
rev: v6.0.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: check-added-large-files
|
|
6
|
+
- id: check-ast
|
|
7
|
+
- id: check-builtin-literals
|
|
8
|
+
- id: check-case-conflict
|
|
9
|
+
- id: check-executables-have-shebangs
|
|
10
|
+
- id: check-illegal-windows-names
|
|
11
|
+
- id: check-json
|
|
12
|
+
- id: check-merge-conflict
|
|
13
|
+
- id: check-shebang-scripts-are-executable
|
|
14
|
+
- id: check-symlinks
|
|
15
|
+
- id: check-toml
|
|
16
|
+
- id: check-vcs-permalinks
|
|
17
|
+
- id: check-xml
|
|
18
|
+
- id: check-yaml
|
|
19
|
+
- id: debug-statements
|
|
20
|
+
- id: destroyed-symlinks
|
|
21
|
+
- id: detect-private-key
|
|
22
|
+
- id: end-of-file-fixer
|
|
23
|
+
- id: fix-byte-order-marker
|
|
24
|
+
- id: forbid-submodules
|
|
25
|
+
- id: name-tests-test
|
|
26
|
+
args:
|
|
27
|
+
- --pytest-test-first
|
|
28
|
+
- id: no-commit-to-branch
|
|
29
|
+
- id: trailing-whitespace
|
|
30
|
+
|
|
31
|
+
- repo: https://github.com/commitizen-tools/commitizen
|
|
32
|
+
rev: v4.13.8
|
|
33
|
+
hooks:
|
|
34
|
+
- id: commitizen
|
|
35
|
+
- id: commitizen-branch
|
|
36
|
+
stages: [pre-push]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
|
|
7
|
+
This project adheres to both [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
|
|
8
|
+
|
|
9
|
+
## [UNRELEASED]
|
|
10
|
+
|
|
11
|
+
## [0.0.3] - 2026-02-26
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- This CHANGELOG file.
|
|
16
|
+
- pre-commit config.
|
|
17
|
+
- test coverage using Codecov
|
|
18
|
+
- LimitedTaskGroup to execute tasks limiting concurrency
|
|
19
|
+
- heartbeat function to run something at regular interval
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Package building and publishing are run in separate jobs to avoid credential sharing.
|
|
24
|
+
- Improved README
|
|
25
|
+
|
|
26
|
+
## [0.0.2] - 2025-12-23
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `TaskGroup` now inherits from `asyncio.TaskGroup`.
|
|
31
|
+
|
|
32
|
+
## [0.0.1] - 2025-12-23
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- `TaskGroup` subclass with a cancel method.
|
|
37
|
+
- `checkpoint` function to yield control to the event loop.
|
|
38
|
+
- `sleep_forever` function.
|
|
39
|
+
|
|
40
|
+
[unreleased]: https://github.com/hartungstenio/asyncio-extensions/compare/0.0.3...HEAD
|
|
41
|
+
[0.0.3]: https://github.com/hartungstenio/asyncio-extensions/compare/0.0.2...0.0.3
|
|
42
|
+
[0.0.2]: https://github.com/hartungstenio/asyncio-extensions/compare/0.0.1...0.0.2
|
|
43
|
+
[0.0.1]: https://github.com/hartungstenio/asyncio-extensions/releases/tag/0.0.1
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: asyncio-extensions
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: Some (maybe) useful extensions for asyncio
|
|
5
|
+
Project-URL: Source, https://github.com/hartungstenio/asyncio-extensions
|
|
6
|
+
Project-URL: Changelog, https://github.com/hartungstenio/asyncio-extensions/blob/main/CHANGELOG.md
|
|
7
|
+
Project-URL: ReleaseNotes, https://github.com/hartungstenio/asyncio-extensions/releases
|
|
8
|
+
Project-URL: Documentation, https://github.com/hartungstenio/asyncio-extensions#readme
|
|
9
|
+
Project-URL: Issues, https://github.com/hartungstenio/asyncio-extensions/issues
|
|
10
|
+
Author-email: Christian Hartung <6785871+hartungstenio@users.noreply.github.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE.txt
|
|
13
|
+
Keywords: asyncio,tasks
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Framework :: AsyncIO
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Programming Language :: Python
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
23
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.11
|
|
27
|
+
Requires-Dist: typing-extensions; python_version < '3.12'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# asyncio-extensions
|
|
31
|
+
|
|
32
|
+
[](https://pypi.org/project/asyncio-extensions)
|
|
33
|
+
[](https://pypi.org/project/asyncio-extensions)
|
|
34
|
+
[](https://codecov.io/github/hartungstenio/asyncio-extensions)
|
|
35
|
+
[](https://results.pre-commit.ci/latest/github/hartungstenio/asyncio-extensions/main)
|
|
36
|
+
|
|
37
|
+
-----
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```console
|
|
42
|
+
pip install asyncio-extensions
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
### TaskGroup
|
|
48
|
+
`asyncio-extensions` provide a cancellable version of AsyncIO's `TaskGroup`.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import asyncio
|
|
52
|
+
|
|
53
|
+
from asyncio_extensions import TaskGroup
|
|
54
|
+
|
|
55
|
+
queue = asyncio.Queue()
|
|
56
|
+
async with TaskGroup() as tg:
|
|
57
|
+
for _ in range(10):
|
|
58
|
+
tg.create_task(consume_from_queue(queue))
|
|
59
|
+
|
|
60
|
+
await add_to_queue(queue)
|
|
61
|
+
await queue.join()
|
|
62
|
+
tg.cancel()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
#### LimitedTaskGroup
|
|
66
|
+
A version of `TaskGroup` which limits the number of running tasks.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import asyncio
|
|
70
|
+
|
|
71
|
+
from asyncio_extensions import LimitedTaskGroup
|
|
72
|
+
|
|
73
|
+
queue = asyncio.Queue()
|
|
74
|
+
async with LimitedTaskGroup(3) as tg:
|
|
75
|
+
for _ in range(50):
|
|
76
|
+
tg.create_task(some_expensive_operation(queue))
|
|
77
|
+
|
|
78
|
+
await add_to_queue(queue)
|
|
79
|
+
await queue.join()
|
|
80
|
+
tg.cancel()
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
### checkpoint
|
|
85
|
+
The `checkpoint` function yields control do the event loop. It is a more elegant approach to do-nothing tasks since they give a chance for other tasks to run.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from asyncio_extensions import checkpoint
|
|
89
|
+
|
|
90
|
+
class DummyChannel:
|
|
91
|
+
async def send_message(self, message):
|
|
92
|
+
await checkpoint()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### sleep_forever
|
|
96
|
+
The `sleep_forever` function never returns. It simply keeps yielding control do the event loop.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from asyncio_extensions import sleep_forever
|
|
100
|
+
|
|
101
|
+
class DummyChannel:
|
|
102
|
+
async def receive_message(self):
|
|
103
|
+
await sleep_forever()
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### heartbeat
|
|
107
|
+
The `heartbeat` function runs a given function at a regular interval.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from asyncio_extensions import heartbeat
|
|
111
|
+
|
|
112
|
+
interval = 5
|
|
113
|
+
|
|
114
|
+
async def ping():
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
async with TaskGroup() as tg:
|
|
118
|
+
tg.create_task(heartbeat(5, ping))
|
|
119
|
+
|
|
120
|
+
await some_long_running_process()
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Contributing
|
|
124
|
+
|
|
125
|
+
Pull requests are welcome. For major changes, please open an issue first
|
|
126
|
+
to discuss what you would like to change.
|
|
127
|
+
|
|
128
|
+
Please make sure to update tests as appropriate.
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
`asyncio-extensions` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# asyncio-extensions
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/asyncio-extensions)
|
|
4
|
+
[](https://pypi.org/project/asyncio-extensions)
|
|
5
|
+
[](https://codecov.io/github/hartungstenio/asyncio-extensions)
|
|
6
|
+
[](https://results.pre-commit.ci/latest/github/hartungstenio/asyncio-extensions/main)
|
|
7
|
+
|
|
8
|
+
-----
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```console
|
|
13
|
+
pip install asyncio-extensions
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
### TaskGroup
|
|
19
|
+
`asyncio-extensions` provide a cancellable version of AsyncIO's `TaskGroup`.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import asyncio
|
|
23
|
+
|
|
24
|
+
from asyncio_extensions import TaskGroup
|
|
25
|
+
|
|
26
|
+
queue = asyncio.Queue()
|
|
27
|
+
async with TaskGroup() as tg:
|
|
28
|
+
for _ in range(10):
|
|
29
|
+
tg.create_task(consume_from_queue(queue))
|
|
30
|
+
|
|
31
|
+
await add_to_queue(queue)
|
|
32
|
+
await queue.join()
|
|
33
|
+
tg.cancel()
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
#### LimitedTaskGroup
|
|
37
|
+
A version of `TaskGroup` which limits the number of running tasks.
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import asyncio
|
|
41
|
+
|
|
42
|
+
from asyncio_extensions import LimitedTaskGroup
|
|
43
|
+
|
|
44
|
+
queue = asyncio.Queue()
|
|
45
|
+
async with LimitedTaskGroup(3) as tg:
|
|
46
|
+
for _ in range(50):
|
|
47
|
+
tg.create_task(some_expensive_operation(queue))
|
|
48
|
+
|
|
49
|
+
await add_to_queue(queue)
|
|
50
|
+
await queue.join()
|
|
51
|
+
tg.cancel()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
### checkpoint
|
|
56
|
+
The `checkpoint` function yields control do the event loop. It is a more elegant approach to do-nothing tasks since they give a chance for other tasks to run.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from asyncio_extensions import checkpoint
|
|
60
|
+
|
|
61
|
+
class DummyChannel:
|
|
62
|
+
async def send_message(self, message):
|
|
63
|
+
await checkpoint()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### sleep_forever
|
|
67
|
+
The `sleep_forever` function never returns. It simply keeps yielding control do the event loop.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from asyncio_extensions import sleep_forever
|
|
71
|
+
|
|
72
|
+
class DummyChannel:
|
|
73
|
+
async def receive_message(self):
|
|
74
|
+
await sleep_forever()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### heartbeat
|
|
78
|
+
The `heartbeat` function runs a given function at a regular interval.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from asyncio_extensions import heartbeat
|
|
82
|
+
|
|
83
|
+
interval = 5
|
|
84
|
+
|
|
85
|
+
async def ping():
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
async with TaskGroup() as tg:
|
|
89
|
+
tg.create_task(heartbeat(5, ping))
|
|
90
|
+
|
|
91
|
+
await some_long_running_process()
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Contributing
|
|
95
|
+
|
|
96
|
+
Pull requests are welcome. For major changes, please open an issue first
|
|
97
|
+
to discuss what you would like to change.
|
|
98
|
+
|
|
99
|
+
Please make sure to update tests as appropriate.
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
`asyncio-extensions` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
|
|
@@ -23,13 +23,17 @@ classifiers = [
|
|
|
23
23
|
"Topic :: Software Development :: Libraries",
|
|
24
24
|
"Typing :: Typed",
|
|
25
25
|
]
|
|
26
|
-
dependencies = [
|
|
26
|
+
dependencies = [
|
|
27
|
+
"typing-extensions; python_version < '3.12'",
|
|
28
|
+
]
|
|
27
29
|
dynamic = ["version"]
|
|
28
30
|
|
|
29
31
|
[project.urls]
|
|
32
|
+
Source = "https://github.com/hartungstenio/asyncio-extensions"
|
|
33
|
+
Changelog = "https://github.com/hartungstenio/asyncio-extensions/blob/main/CHANGELOG.md"
|
|
34
|
+
ReleaseNotes = "https://github.com/hartungstenio/asyncio-extensions/releases"
|
|
30
35
|
Documentation = "https://github.com/hartungstenio/asyncio-extensions#readme"
|
|
31
36
|
Issues = "https://github.com/hartungstenio/asyncio-extensions/issues"
|
|
32
|
-
Source = "https://github.com/hartungstenio/asyncio-extensions"
|
|
33
37
|
|
|
34
38
|
[dependency-groups]
|
|
35
39
|
dev = [
|
|
@@ -86,6 +90,12 @@ extra-dependencies = [
|
|
|
86
90
|
[[tool.hatch.envs.hatch-test.matrix]]
|
|
87
91
|
python = ["3.11", "3.12", "3.13", "3.14"]
|
|
88
92
|
|
|
93
|
+
[tool.hatch.envs.hatch-test.scripts]
|
|
94
|
+
run = "pytest{env:HATCH_TEST_ARGS:} {args}"
|
|
95
|
+
run-cov = "coverage run -m pytest{env:HATCH_TEST_ARGS:} {args}"
|
|
96
|
+
cov-combine = "coverage combine"
|
|
97
|
+
cov-report = "coverage xml"
|
|
98
|
+
|
|
89
99
|
[tool.pytest]
|
|
90
100
|
asyncio_default_fixture_loop_scope = "function"
|
|
91
101
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Utility functions and extensions for AsyncIO."""
|
|
2
|
+
|
|
3
|
+
from .taskgroups import (
|
|
4
|
+
LimitedTaskGroup,
|
|
5
|
+
TaskGroup,
|
|
6
|
+
TerminateTaskGroup,
|
|
7
|
+
force_terminate_task_group,
|
|
8
|
+
)
|
|
9
|
+
from .utils import checkpoint, heartbeat, sleep_forever
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"LimitedTaskGroup",
|
|
13
|
+
"TaskGroup",
|
|
14
|
+
"TerminateTaskGroup",
|
|
15
|
+
"checkpoint",
|
|
16
|
+
"force_terminate_task_group",
|
|
17
|
+
"heartbeat",
|
|
18
|
+
"sleep_forever",
|
|
19
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Extensions to interact with TaskGroups."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Coroutine
|
|
6
|
+
from contextvars import Context
|
|
7
|
+
from types import TracebackType
|
|
8
|
+
from typing import Any, Never, TypedDict, TypeVar, Unpack
|
|
9
|
+
|
|
10
|
+
from ._compat import override
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _CreateTaskParams(TypedDict, total=False):
|
|
16
|
+
"""Parameters for creating a task in a TaskGroup."""
|
|
17
|
+
|
|
18
|
+
name: str | None
|
|
19
|
+
context: Context | None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if sys.version_info >= (3, 14):
|
|
23
|
+
|
|
24
|
+
class CreateTaskParams(_CreateTaskParams):
|
|
25
|
+
"""Parameters for creating a task in a TaskGroup."""
|
|
26
|
+
|
|
27
|
+
eager_start: bool | None
|
|
28
|
+
else:
|
|
29
|
+
CreateTaskParams = _CreateTaskParams
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TerminateTaskGroup(BaseException):
|
|
33
|
+
"""Signal to terminate a task group.
|
|
34
|
+
|
|
35
|
+
Technically not an exception, so inherits from `BaseException`.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def force_terminate_task_group() -> Never:
|
|
40
|
+
"""Force termination of a task group."""
|
|
41
|
+
raise TerminateTaskGroup
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TaskGroup(asyncio.TaskGroup):
|
|
45
|
+
"""A version of asyncio.TaskGroup with a cancel method."""
|
|
46
|
+
|
|
47
|
+
async def __aexit__( # noqa: D105
|
|
48
|
+
self,
|
|
49
|
+
exc_type: type[BaseException] | None,
|
|
50
|
+
exc_value: BaseException | None,
|
|
51
|
+
traceback: TracebackType | None,
|
|
52
|
+
) -> None:
|
|
53
|
+
try: # noqa: SIM105
|
|
54
|
+
await super().__aexit__(exc_type, exc_value, traceback)
|
|
55
|
+
except* TerminateTaskGroup:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
def cancel(self) -> None:
|
|
59
|
+
"""Cancel any remaining task in the TaskGroup."""
|
|
60
|
+
self.create_task(force_terminate_task_group())
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class LimitedTaskGroup(TaskGroup):
|
|
64
|
+
"""A TaskGroup that limits the number of concurrent tasks."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, max_concurrent: int) -> None:
|
|
67
|
+
"""Initialize the LimitedTaskGroup.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
max_concurrent: Maximum number of concurrent tasks.
|
|
71
|
+
"""
|
|
72
|
+
super().__init__()
|
|
73
|
+
self._semaphore = asyncio.Semaphore(max_concurrent)
|
|
74
|
+
|
|
75
|
+
async def _task_wrapper(self, coro: Coroutine[Any, Any, T]) -> T:
|
|
76
|
+
async with self._semaphore:
|
|
77
|
+
return await coro
|
|
78
|
+
|
|
79
|
+
@override
|
|
80
|
+
def create_task(
|
|
81
|
+
self,
|
|
82
|
+
coro: Coroutine[Any, Any, T],
|
|
83
|
+
**kwargs: Unpack[CreateTaskParams],
|
|
84
|
+
) -> asyncio.Task[T]:
|
|
85
|
+
"""Create a task within the limited concurrency context.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
coro: The coroutine to run as a task.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The created asyncio Task.
|
|
92
|
+
"""
|
|
93
|
+
return super().create_task(self._task_wrapper(coro), **kwargs)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Utility functions."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Awaitable, Callable, Generator
|
|
5
|
+
from typing import Any, Never, ParamSpec, TypeVar
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
P = ParamSpec("P")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class YieldToEventLoop:
|
|
12
|
+
"""Helper class to give control back to the event loop."""
|
|
13
|
+
|
|
14
|
+
def __await__(self) -> Generator[None, Any, None]:
|
|
15
|
+
"""Yield control to the event loop."""
|
|
16
|
+
yield
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def checkpoint() -> None:
|
|
20
|
+
"""
|
|
21
|
+
Give control back to the eventloop.
|
|
22
|
+
|
|
23
|
+
This has the same effect of and asyncio.sleep(0), but it is more semantic
|
|
24
|
+
"""
|
|
25
|
+
return await YieldToEventLoop()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def sleep_forever() -> Never:
|
|
29
|
+
"""Sleeps forever."""
|
|
30
|
+
while True:
|
|
31
|
+
await YieldToEventLoop()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def noop(result: T) -> T:
|
|
35
|
+
"""Give control back to the event loop once, then returns the given argument."""
|
|
36
|
+
await YieldToEventLoop()
|
|
37
|
+
return result
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def heartbeat(
|
|
41
|
+
interval: float,
|
|
42
|
+
fn: Callable[P, Awaitable[T]],
|
|
43
|
+
*args: P.args,
|
|
44
|
+
**kwargs: P.kwargs,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Run :arg:`fn` with the given args at regular interval."""
|
|
47
|
+
while True:
|
|
48
|
+
await asyncio.sleep(interval)
|
|
49
|
+
await fn(*args, **kwargs)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
from contextvars import copy_context
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from asyncio_extensions import checkpoint
|
|
8
|
+
from asyncio_extensions.taskgroups import (
|
|
9
|
+
LimitedTaskGroup,
|
|
10
|
+
TaskGroup,
|
|
11
|
+
TerminateTaskGroup,
|
|
12
|
+
force_terminate_task_group,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from . import noop
|
|
16
|
+
|
|
17
|
+
pytestmark = pytest.mark.asyncio
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def test_force_terminate_task_group() -> None:
|
|
21
|
+
with pytest.RaisesGroup(TerminateTaskGroup):
|
|
22
|
+
async with asyncio.TaskGroup() as tg:
|
|
23
|
+
tg.create_task(asyncio.sleep(1000))
|
|
24
|
+
tg.create_task(force_terminate_task_group())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestTaskGroup:
|
|
28
|
+
async def test_runs_tasks(self) -> None:
|
|
29
|
+
async with TaskGroup() as tg:
|
|
30
|
+
tasks = [tg.create_task(noop()) for _ in range(10)]
|
|
31
|
+
|
|
32
|
+
assert all(t.done() for t in tasks)
|
|
33
|
+
assert not any(t.cancelled() for t in tasks)
|
|
34
|
+
|
|
35
|
+
async def test_cancel_with_running_task(self) -> None:
|
|
36
|
+
async with (
|
|
37
|
+
asyncio.timeout(1),
|
|
38
|
+
TaskGroup() as tg,
|
|
39
|
+
):
|
|
40
|
+
task = tg.create_task(asyncio.sleep(10))
|
|
41
|
+
tg.cancel()
|
|
42
|
+
assert task.cancelled() is True
|
|
43
|
+
|
|
44
|
+
async def test_cancel_without_running_task(self) -> None:
|
|
45
|
+
async with TaskGroup() as tg:
|
|
46
|
+
tg.cancel()
|
|
47
|
+
|
|
48
|
+
async def test_cancel_with_done_task(self) -> None:
|
|
49
|
+
async with (
|
|
50
|
+
asyncio.timeout(1),
|
|
51
|
+
TaskGroup() as tg,
|
|
52
|
+
):
|
|
53
|
+
task = tg.create_task(noop())
|
|
54
|
+
await task
|
|
55
|
+
tg.cancel()
|
|
56
|
+
|
|
57
|
+
assert task.done() is True
|
|
58
|
+
assert task.cancelled() is False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TestLimitedTaskGroup:
|
|
62
|
+
async def test_limits_concurrency(self) -> None:
|
|
63
|
+
max_concurrent = 3
|
|
64
|
+
current_concurrent = 0
|
|
65
|
+
peak_concurrent = 0
|
|
66
|
+
|
|
67
|
+
async def task() -> None:
|
|
68
|
+
nonlocal current_concurrent, peak_concurrent
|
|
69
|
+
current_concurrent += 1
|
|
70
|
+
peak_concurrent = max(peak_concurrent, current_concurrent)
|
|
71
|
+
await checkpoint()
|
|
72
|
+
current_concurrent -= 1
|
|
73
|
+
|
|
74
|
+
async with LimitedTaskGroup(max_concurrent=max_concurrent) as tg:
|
|
75
|
+
for _ in range(100):
|
|
76
|
+
tg.create_task(task())
|
|
77
|
+
|
|
78
|
+
assert peak_concurrent <= max_concurrent
|
|
79
|
+
|
|
80
|
+
async def test_create_task_params(self) -> None:
|
|
81
|
+
ctx = copy_context()
|
|
82
|
+
async with LimitedTaskGroup(max_concurrent=2) as tg:
|
|
83
|
+
task = tg.create_task(noop(), name="test_task", context=ctx)
|
|
84
|
+
|
|
85
|
+
assert task.get_name() == "test_task"
|
|
86
|
+
|
|
87
|
+
if sys.version_info >= (3, 12):
|
|
88
|
+
assert task.get_context() is ctx
|
|
89
|
+
|
|
90
|
+
@pytest.mark.skipif(sys.version_info < (3, 14), reason="Requires Python 3.14+")
|
|
91
|
+
async def test_create_task_params_eager(self) -> None:
|
|
92
|
+
ctx = copy_context()
|
|
93
|
+
async with LimitedTaskGroup(max_concurrent=2) as tg:
|
|
94
|
+
task = tg.create_task(
|
|
95
|
+
noop(),
|
|
96
|
+
name="test_task",
|
|
97
|
+
context=ctx,
|
|
98
|
+
eager_start=True,
|
|
99
|
+
)
|
|
100
|
+
tg.cancel()
|
|
101
|
+
|
|
102
|
+
assert task.get_name() == "test_task"
|
|
103
|
+
assert task.get_context() is ctx
|
|
104
|
+
assert task.done() is True
|
|
105
|
+
|
|
106
|
+
async def test_can_cancel(self) -> None:
|
|
107
|
+
tg = LimitedTaskGroup(max_concurrent=2)
|
|
108
|
+
assert hasattr(tg, "cancel")
|
|
109
|
+
assert callable(tg.cancel)
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from contextlib import suppress
|
|
3
|
+
from unittest import mock
|
|
2
4
|
|
|
3
5
|
import pytest
|
|
4
6
|
from faker import Faker
|
|
5
7
|
|
|
6
|
-
from asyncio_extensions.utils import checkpoint, sleep_forever
|
|
8
|
+
from asyncio_extensions.utils import checkpoint, heartbeat, sleep_forever
|
|
7
9
|
|
|
8
|
-
from .
|
|
10
|
+
from . import noop
|
|
9
11
|
|
|
10
12
|
pytestmark = pytest.mark.asyncio
|
|
11
13
|
|
|
@@ -36,3 +38,12 @@ async def test_sleep_forever_cycles_event_loop(faker: Faker) -> None:
|
|
|
36
38
|
|
|
37
39
|
assert all(t.done() for t in tasks)
|
|
38
40
|
assert not any(t.cancelled() for t in tasks)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def test_heartbeat() -> None:
|
|
44
|
+
mock_fn = mock.AsyncMock()
|
|
45
|
+
with suppress(TimeoutError):
|
|
46
|
+
async with asyncio.timeout(5):
|
|
47
|
+
await heartbeat(1, mock_fn, 1, 2, name="a")
|
|
48
|
+
|
|
49
|
+
assert mock_fn.await_args_list == [mock.call(1, 2, name="a")] * 4
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
name: Build & Release
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
release:
|
|
5
|
-
types: [published]
|
|
6
|
-
|
|
7
|
-
permissions:
|
|
8
|
-
contents: read
|
|
9
|
-
|
|
10
|
-
jobs:
|
|
11
|
-
deploy:
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
environment:
|
|
14
|
-
name: release
|
|
15
|
-
url: https://pypi.org/p/asyncio-extensions
|
|
16
|
-
permissions:
|
|
17
|
-
id-token: write
|
|
18
|
-
steps:
|
|
19
|
-
- uses: actions/checkout@v6
|
|
20
|
-
|
|
21
|
-
- uses: actions/setup-python@v6
|
|
22
|
-
with:
|
|
23
|
-
python-version: "3.x"
|
|
24
|
-
|
|
25
|
-
- name: Install dependencies
|
|
26
|
-
run: |
|
|
27
|
-
python -m pip install --upgrade pip build
|
|
28
|
-
|
|
29
|
-
- name: Build package
|
|
30
|
-
run: python -m build
|
|
31
|
-
|
|
32
|
-
- name: Publish package
|
|
33
|
-
uses: pypa/gh-action-pypi-publish@v1.13.0
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: asyncio-extensions
|
|
3
|
-
Version: 0.0.2
|
|
4
|
-
Summary: Some (maybe) useful extensions for asyncio
|
|
5
|
-
Project-URL: Documentation, https://github.com/hartungstenio/asyncio-extensions#readme
|
|
6
|
-
Project-URL: Issues, https://github.com/hartungstenio/asyncio-extensions/issues
|
|
7
|
-
Project-URL: Source, https://github.com/hartungstenio/asyncio-extensions
|
|
8
|
-
Author-email: Christian Hartung <6785871+hartungstenio@users.noreply.github.com>
|
|
9
|
-
License-Expression: MIT
|
|
10
|
-
License-File: LICENSE.txt
|
|
11
|
-
Keywords: asyncio,tasks
|
|
12
|
-
Classifier: Development Status :: 4 - Beta
|
|
13
|
-
Classifier: Framework :: AsyncIO
|
|
14
|
-
Classifier: Intended Audience :: Developers
|
|
15
|
-
Classifier: Programming Language :: Python
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
-
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
21
|
-
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
22
|
-
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
-
Classifier: Typing :: Typed
|
|
24
|
-
Requires-Python: >=3.11
|
|
25
|
-
Description-Content-Type: text/markdown
|
|
26
|
-
|
|
27
|
-
# asyncio-extensions
|
|
28
|
-
|
|
29
|
-
[](https://pypi.org/project/asyncio-extensions)
|
|
30
|
-
[](https://pypi.org/project/asyncio-extensions)
|
|
31
|
-
|
|
32
|
-
-----
|
|
33
|
-
|
|
34
|
-
## Table of Contents
|
|
35
|
-
|
|
36
|
-
- [Installation](#installation)
|
|
37
|
-
- [License](#license)
|
|
38
|
-
|
|
39
|
-
## Installation
|
|
40
|
-
|
|
41
|
-
```console
|
|
42
|
-
pip install asyncio-extensions
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## License
|
|
46
|
-
|
|
47
|
-
`asyncio-extensions` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# asyncio-extensions
|
|
2
|
-
|
|
3
|
-
[](https://pypi.org/project/asyncio-extensions)
|
|
4
|
-
[](https://pypi.org/project/asyncio-extensions)
|
|
5
|
-
|
|
6
|
-
-----
|
|
7
|
-
|
|
8
|
-
## Table of Contents
|
|
9
|
-
|
|
10
|
-
- [Installation](#installation)
|
|
11
|
-
- [License](#license)
|
|
12
|
-
|
|
13
|
-
## Installation
|
|
14
|
-
|
|
15
|
-
```console
|
|
16
|
-
pip install asyncio-extensions
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## License
|
|
20
|
-
|
|
21
|
-
`asyncio-extensions` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
"""Utility functions and extensions for AsyncIO."""
|
|
2
|
-
|
|
3
|
-
from .taskgroups import TaskGroup, TerminateTaskGroup, force_terminate_task_group
|
|
4
|
-
from .utils import checkpoint, sleep_forever
|
|
5
|
-
|
|
6
|
-
__all__ = [
|
|
7
|
-
"TaskGroup",
|
|
8
|
-
"TerminateTaskGroup",
|
|
9
|
-
"checkpoint",
|
|
10
|
-
"force_terminate_task_group",
|
|
11
|
-
"sleep_forever",
|
|
12
|
-
]
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
"""Extensions to interact with TaskGroups."""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
from types import TracebackType
|
|
5
|
-
from typing import Never, TypeVar
|
|
6
|
-
|
|
7
|
-
T = TypeVar("T")
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class TerminateTaskGroup(BaseException):
|
|
11
|
-
"""Signal to terminate a task group.
|
|
12
|
-
|
|
13
|
-
Technically not an exception, so inherits from `BaseException`.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
async def force_terminate_task_group() -> Never:
|
|
18
|
-
"""Force termination of a task group."""
|
|
19
|
-
raise TerminateTaskGroup
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class TaskGroup(asyncio.TaskGroup):
|
|
23
|
-
"""A version of asyncio.TaskGroup with a cancel method."""
|
|
24
|
-
|
|
25
|
-
async def __aexit__( # noqa: D105
|
|
26
|
-
self,
|
|
27
|
-
exc_type: type[BaseException] | None,
|
|
28
|
-
exc_value: BaseException | None,
|
|
29
|
-
traceback: TracebackType | None,
|
|
30
|
-
) -> None:
|
|
31
|
-
try: # noqa: SIM105
|
|
32
|
-
await super().__aexit__(exc_type, exc_value, traceback)
|
|
33
|
-
except* TerminateTaskGroup:
|
|
34
|
-
pass
|
|
35
|
-
|
|
36
|
-
def cancel(self) -> None:
|
|
37
|
-
"""Cancel any remaining task in the TaskGroup."""
|
|
38
|
-
self.create_task(force_terminate_task_group())
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
"""Utility functions."""
|
|
2
|
-
|
|
3
|
-
from collections.abc import Generator
|
|
4
|
-
from typing import Any, Never
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class YieldToEventLoop:
|
|
8
|
-
"""Helper class to give control back to the event loop."""
|
|
9
|
-
|
|
10
|
-
def __await__(self) -> Generator[None, Any, None]:
|
|
11
|
-
"""Yield control to the event loop."""
|
|
12
|
-
yield
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
async def checkpoint() -> None:
|
|
16
|
-
"""
|
|
17
|
-
Give control back to the eventloop.
|
|
18
|
-
|
|
19
|
-
This has the same effect of and asyncio.sleep(0), but it is more semantic
|
|
20
|
-
"""
|
|
21
|
-
return await YieldToEventLoop()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
async def sleep_forever() -> Never:
|
|
25
|
-
"""Sleeps forever."""
|
|
26
|
-
while True:
|
|
27
|
-
await YieldToEventLoop()
|
|
File without changes
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
|
|
5
|
-
from asyncio_extensions.taskgroups import (
|
|
6
|
-
TaskGroup,
|
|
7
|
-
TerminateTaskGroup,
|
|
8
|
-
force_terminate_task_group,
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
from .utils import noop
|
|
12
|
-
|
|
13
|
-
pytestmark = pytest.mark.asyncio
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
async def test_force_terminate_task_group() -> None:
|
|
17
|
-
with pytest.RaisesGroup(TerminateTaskGroup):
|
|
18
|
-
async with asyncio.TaskGroup() as tg:
|
|
19
|
-
tg.create_task(asyncio.sleep(1000))
|
|
20
|
-
tg.create_task(force_terminate_task_group())
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class TestTaskGroup:
|
|
24
|
-
async def test_runs_tasks(self) -> None:
|
|
25
|
-
async with TaskGroup() as tg:
|
|
26
|
-
tasks = [tg.create_task(noop()) for _ in range(10)]
|
|
27
|
-
|
|
28
|
-
assert all(t.done() for t in tasks)
|
|
29
|
-
assert not any(t.cancelled() for t in tasks)
|
|
30
|
-
|
|
31
|
-
async def test_cancel_with_running_task(self) -> None:
|
|
32
|
-
async with (
|
|
33
|
-
asyncio.timeout(1),
|
|
34
|
-
TaskGroup() as tg,
|
|
35
|
-
):
|
|
36
|
-
task = tg.create_task(asyncio.sleep(10))
|
|
37
|
-
tg.cancel()
|
|
38
|
-
assert task.cancelled() is True
|
|
39
|
-
|
|
40
|
-
async def test_cancel_without_running_task(self) -> None:
|
|
41
|
-
async with TaskGroup() as tg:
|
|
42
|
-
tg.cancel()
|
|
43
|
-
|
|
44
|
-
async def test_cancel_with_done_task(self) -> None:
|
|
45
|
-
async with (
|
|
46
|
-
asyncio.timeout(1),
|
|
47
|
-
TaskGroup() as tg,
|
|
48
|
-
):
|
|
49
|
-
task = tg.create_task(noop())
|
|
50
|
-
await task
|
|
51
|
-
tg.cancel()
|
|
52
|
-
|
|
53
|
-
assert task.done() is True
|
|
54
|
-
assert task.cancelled() is False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|