Skip to content

Commit f0e61d3

Browse files
committed
initial commit
1 parent 73a3684 commit f0e61d3

File tree

12 files changed

+380
-2
lines changed

12 files changed

+380
-2
lines changed

.flake8

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[flake8]
2+
# Ignore style and complexity
3+
# E: style errors
4+
# W: style warnings
5+
# C: complexity
6+
# D: docstrings
7+
# I: import style
8+
ignore = E, C, W, I
9+
exclude =
10+
.cache,
11+
.github,
12+
docs,
13+
build,
14+
dist,

.github/workflows/publish.yaml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Build releases and (on tags) publish to PyPI
2+
name: Release
3+
4+
# always build releases (to make sure wheel-building works)
5+
# but only publish to PyPI on tags
6+
on:
7+
push:
8+
pull_request:
9+
10+
jobs:
11+
build-release:
12+
runs-on: ubuntu-20.04
13+
steps:
14+
- uses: actions/checkout@v2
15+
- uses: actions/setup-python@v2
16+
with:
17+
python-version: 3.9
18+
19+
- name: install build package
20+
run: |
21+
pip install --upgrade pip
22+
pip install build
23+
pip freeze
24+
25+
- name: build release
26+
run: |
27+
python -m build --sdist --wheel .
28+
ls -l dist
29+
30+
- name: publish to pypi
31+
uses: pypa/[email protected]
32+
if: startsWith(github.ref, 'refs/tags/')
33+
with:
34+
user: __token__
35+
password: ${{ secrets.pypi_password }}

.github/workflows/test.yaml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# This is a GitHub workflow defining a set of jobs with a set of steps.
2+
# ref: https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions
3+
#
4+
name: Tests
5+
6+
on:
7+
pull_request:
8+
push:
9+
workflow_dispatch:
10+
11+
jobs:
12+
# Run tests
13+
test:
14+
runs-on: ubuntu-20.04
15+
timeout-minutes: 10
16+
17+
strategy:
18+
# Keep running even if one variation of the job fail
19+
fail-fast: false
20+
matrix:
21+
python:
22+
- "3.6"
23+
- "3.7"
24+
- "3.8"
25+
- "3.9"
26+
- "3.10"
27+
28+
steps:
29+
- uses: actions/checkout@v2
30+
31+
- name: Install Python ${{ matrix.python }}
32+
uses: actions/setup-python@v2
33+
with:
34+
python-version: ${{ matrix.python }}
35+
36+
# preserve pip cache to speed up installation
37+
- name: Cache pip
38+
uses: actions/cache@v2
39+
with:
40+
path: ~/.cache/pip
41+
# Look to see if there is a cache hit for the corresponding requirements file
42+
key: ${{ runner.os }}-pip-${{ hashFiles('*requirements.txt') }}
43+
restore-keys: |
44+
${{ runner.os }}-pip-
45+
46+
- name: Install Python dependencies
47+
run: |
48+
pip install --upgrade pip
49+
pip install --upgrade .[test] -r dev-requirements.txt
50+
pip freeze
51+
52+
- name: Run tests
53+
# FIXME: --color=yes explicitly set because:
54+
# https://github.com/actions/runner/issues/241
55+
run: |
56+
pytest -v --color=yes --cov=asyncio_atexit
57+
58+
- name: Submit codecov report
59+
run: |
60+
codecov

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ __pycache__/
33
*.py[cod]
44
*$py.class
55

6+
.DS_Store
7+
68
# C extensions
79
*.so
810

MANIFEST.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
include test_*.py
2+
graft tests

README.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,28 @@
1-
# asyncio-atexit
2-
atexit, but for asyncio
1+
# asyncio atexit
2+
3+
Adds atexit functionality to asyncio:
4+
5+
```python
6+
import asyncio_atexit
7+
8+
async def close_db():
9+
await db_connection.close()
10+
11+
asyncio_atexit.register(close_db)
12+
```
13+
14+
[atexit][] is part of the standard library,
15+
and gives you a way to register functions to call when the interpreter exits.
16+
17+
[atexit]: https://docs.python.org/3/library/atexit.html
18+
19+
asyncio doesn't have equivalent functionality to register functions
20+
when the _event loop_ exits:
21+
22+
This package adds functionality that can be considered equivalent to `atexit.register`,
23+
but tied to the event loop lifecycle. It:
24+
25+
1. accepts both coroutines and synchronous functions
26+
1. should be called from a running event loop
27+
1. calls registered cleanup functions when the event loop closes
28+
1. only works if the application running the event loop calls `close()`

asyncio_atexit.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
"""
2+
asyncio_atexit: atexit for asyncio
3+
"""
4+
5+
import asyncio
6+
import inspect
7+
import sys
8+
import weakref
9+
from functools import partial
10+
11+
__all__ = ["register", "unregister"]
12+
__version__ = "1.0.0.dev"
13+
14+
_registry = weakref.WeakKeyDictionary()
15+
16+
if sys.version_info < (3, 7):
17+
get_running_loop = asyncio.get_event_loop
18+
else:
19+
get_running_loop = asyncio.get_running_loop
20+
21+
22+
class _RegistryEntry:
23+
def __init__(self, loop):
24+
self._close_ref = weakref.WeakMethod(loop.close)
25+
self.callbacks = []
26+
27+
def close(self):
28+
return self._close_ref()()
29+
30+
31+
def register(callback, *, loop=None):
32+
"""
33+
Register a callback for when the current event loop closes
34+
35+
Like atexit.register, but run when the asyncio loop is closing,
36+
rather than process cleanup.
37+
38+
`loop` may be specified as a keyword arg
39+
to attach to a non-running event loop.
40+
41+
Allows coroutines to cleanup their resources.
42+
43+
Callback will be passed no arguments.
44+
To pass arguments to your callback,
45+
use `functools.partial`.
46+
"""
47+
entry = _get_entry(loop)
48+
entry.callbacks.append(callback)
49+
50+
51+
def unregister(callback, *, loop=None):
52+
"""
53+
Unregister a callback registered with asyncio_atexit.register
54+
55+
`loop` may be specified as a keyword arg
56+
to attach to a non-running event loop.
57+
"""
58+
59+
entry = _get_entry(loop)
60+
# remove all instances of the callback
61+
while True:
62+
try:
63+
entry.callbacks.remove(callback)
64+
except ValueError:
65+
break
66+
67+
68+
def _get_entry(loop=None):
69+
"""Get the registry entry for an event loop"""
70+
if loop is None:
71+
loop = get_running_loop()
72+
_register_loop(loop)
73+
return _registry[loop]
74+
75+
76+
def _register_loop(loop):
77+
"""Patch an asyncio.EventLoop to support atexit callbacks"""
78+
if loop in _registry:
79+
return
80+
81+
_registry[loop] = _RegistryEntry(loop)
82+
83+
loop.close = partial(_asyncio_atexit_close, loop)
84+
85+
86+
async def _run_asyncio_atexits(loop, callbacks):
87+
"""Run asyncio atexit callbacks
88+
89+
This runs in EventLoop.close() prior to actually closing the loop
90+
"""
91+
for callback in callbacks:
92+
try:
93+
f = callback()
94+
if inspect.isawaitable(f):
95+
await f
96+
except Exception as e:
97+
print(
98+
f"Unhandled exception in asyncio atexit callback {callback}: {e}",
99+
file=sys.stderr,
100+
)
101+
102+
103+
def _asyncio_atexit_close(loop):
104+
"""Patched EventLoop.close method to run atexit callbacks
105+
106+
prior to the unpatched close method.
107+
"""
108+
entry = _get_entry(loop)
109+
if entry.callbacks:
110+
loop.run_until_complete(_run_asyncio_atexits(loop, entry.callbacks))
111+
entry.callbacks[:] = []
112+
return entry.close()

dev-requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
codecov
2+
pytest-cov

pyproject.toml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
[tool.isort]
2+
profile = "black"
3+
multi_line_output = 3
4+
5+
[build-system]
6+
requires = [
7+
"setuptools",
8+
"wheel",
9+
]
10+
build-backend = "setuptools.build_meta"
11+
12+
[tool.tbump]
13+
# Uncomment this if your project is hosted on GitHub:
14+
github_url = "https://github.com/minrk/asyncio-atexit"
15+
16+
[tool.tbump.version]
17+
current = "1.0.0.dev"
18+
19+
# Example of a semver regexp.
20+
# Make sure this matches current_version before
21+
# using tbump
22+
regex = '''
23+
(?P<major>\d+)
24+
\.
25+
(?P<minor>\d+)
26+
\.
27+
(?P<patch>\d+)
28+
(?P<pre>((a|b|rc)\d+)|)
29+
\.?
30+
(?P<dev>(?<=\.)dev\d*|)
31+
'''
32+
33+
[tool.tbump.git]
34+
message_template = "Bump to {new_version}"
35+
tag_template = "{new_version}"
36+
37+
# For each file to patch, add a [[tool.tbump.file]] config
38+
# section containing the path of the file, relative to the
39+
# pyproject.toml location.
40+
41+
[[tool.tbump.file]]
42+
src = "asyncio_atexit.py"

setup.cfg

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[metadata]
2+
name = asyncio-atexit
3+
version = attr: asyncio_atexit.__version__
4+
description = Like atexit, but for asyncio
5+
long_description = file: README.md
6+
long_description_content_type = text/markdown
7+
author = Min RK
8+
author_email = [email protected]
9+
url = https://github.com/minrk/asyncio-atexit
10+
keywords = asyncio
11+
license = MIT License
12+
classifiers =
13+
Framework :: AsyncIO
14+
License :: OSI Approved :: MIT License
15+
Programming Language :: Python :: 3
16+
Programming Language :: Python :: 3.6
17+
18+
[options]
19+
py_modules = asyncio_atexit
20+
python_requires = >=3.6
21+
22+
[options.extras_require]
23+
test = pytest
24+
25+
[tool:pytest]
26+
# asyncio_mode = auto

0 commit comments

Comments
 (0)