Skip to content

Commit 9f82bd7

Browse files
1st1fantix
andauthored
Port uvloop to Python 3.12 (#570)
* Bump Cython to 0.29.36 * Add missing new API -- timeout param to shutdown_default_executor * Stop using the beloved, but now deprecated, 'IF' * Explicitly qualify void-returning callback functions as `noexcept`. * Fix test_libuv_get_loop_t_ptr to work under Python 3.12 * "Fix" the failing asyncio tests by adding a sleep() call * Add 3.12 to CI scripts * Stop configuring watchers for asyncio tests under 3.12+ * Add the new timeout parameter of shutdown_default_executor to typeshed * Implement uvloop.run() * Add pyproject.toml and update CI Co-authored-by: Fantix King <[email protected]>
1 parent 0687643 commit 9f82bd7

33 files changed

+468
-157
lines changed

.github/workflows/release.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ jobs:
7575
strategy:
7676
matrix:
7777
os: [ubuntu-latest, macos-latest]
78-
cibw_python: ["cp37-*", "cp38-*", "cp39-*", "cp310-*", "cp311-*"]
78+
cibw_python: ["cp37-*", "cp38-*", "cp39-*", "cp310-*", "cp311-*", "cp312-*"]
7979
cibw_arch: ["x86_64", "aarch64", "universal2"]
8080
exclude:
8181
- os: ubuntu-latest
@@ -110,14 +110,11 @@ jobs:
110110
run: |
111111
brew install gnu-sed libtool autoconf automake
112112
113-
- uses: pypa/cibuildwheel@v2.9.0
113+
- uses: pypa/cibuildwheel@v2.16.2
114114
env:
115115
CIBW_BUILD_VERBOSITY: 1
116116
CIBW_BUILD: ${{ matrix.cibw_python }}
117117
CIBW_ARCHS: ${{ matrix.cibw_arch }}
118-
CIBW_TEST_EXTRAS: "test"
119-
CIBW_TEST_COMMAND: "python -m unittest discover -v {project}/tests"
120-
CIBW_TEST_COMMAND_WINDOWS: "python -m unittest discover -v {project}\\tests"
121118
CIBW_TEST_SKIP: "*universal2:arm64"
122119

123120
- uses: actions/upload-artifact@v3

.github/workflows/tests.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
runs-on: ${{ matrix.os }}
1515
strategy:
1616
matrix:
17-
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
17+
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
1818
os: [ubuntu-latest, macos-latest]
1919

2020
env:
@@ -58,7 +58,8 @@ jobs:
5858
make test
5959
6060
- name: Test (debug build)
61-
if: steps.release.outputs.version == 0
61+
# XXX Re-enable 3.12 once we migrate to Cython 3
62+
if: steps.release.outputs.version == 0 && matrix.python-version != '3.12'
6263
run: |
6364
make distclean && make debug && make test
6465

README.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,27 @@ uvloop with::
5353
Using uvloop
5454
------------
5555

56+
As of uvloop 0.18, the preferred way of using it is via the
57+
``uvloop.run()`` helper function:
58+
59+
60+
.. code:: python
61+
62+
import uvloop
63+
64+
async def main():
65+
# Main entry-point.
66+
...
67+
68+
uvloop.run(main())
69+
70+
``uvloop.run()`` works by simply configuring ``asyncio.run()``
71+
to use uvloop, passing all of the arguments to it, such as ``debug``,
72+
e.g. ``uvloop.run(main(), debug=True)``.
73+
74+
With Python 3.11 and earlier the following alternative
75+
snippet can be used:
76+
5677
.. code:: python
5778
5879
import asyncio

pyproject.toml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
[project]
2+
name = "uvloop"
3+
description = "Fast implementation of asyncio event loop on top of libuv"
4+
authors = [{name = "Yury Selivanov", email = "[email protected]"}]
5+
requires-python = '>=3.7.0'
6+
readme = "README.rst"
7+
license = {text = "MIT License"}
8+
dynamic = ["version"]
9+
keywords = [
10+
"asyncio",
11+
"networking",
12+
]
13+
classifiers = [
14+
"Development Status :: 5 - Production/Stable",
15+
"Framework :: AsyncIO",
16+
"Intended Audience :: Developers",
17+
"License :: OSI Approved :: Apache Software License",
18+
"License :: OSI Approved :: MIT License",
19+
"Operating System :: POSIX",
20+
"Operating System :: MacOS :: MacOS X",
21+
"Programming Language :: Python :: 3 :: Only",
22+
"Programming Language :: Python :: 3.7",
23+
"Programming Language :: Python :: 3.8",
24+
"Programming Language :: Python :: 3.9",
25+
"Programming Language :: Python :: 3.10",
26+
"Programming Language :: Python :: 3.11",
27+
"Programming Language :: Python :: 3.12",
28+
"Programming Language :: Python :: Implementation :: CPython",
29+
"Topic :: System :: Networking",
30+
]
31+
32+
[project.urls]
33+
github = "https://github.com/MagicStack/uvloop"
34+
35+
[project.optional-dependencies]
36+
test = [
37+
# pycodestyle is a dependency of flake8, but it must be frozen because
38+
# their combination breaks too often
39+
# (example breakage: https://gitlab.com/pycqa/flake8/issues/427)
40+
'aiohttp>=3.8.1; python_version < "3.12"',
41+
'aiohttp==3.9.0b0; python_version >= "3.12"',
42+
'flake8~=5.0',
43+
'psutil',
44+
'pycodestyle~=2.9.0',
45+
'pyOpenSSL~=23.0.0',
46+
'mypy>=0.800',
47+
'Cython(>=0.29.36,<0.30.0)',
48+
]
49+
docs = [
50+
'Sphinx~=4.1.2',
51+
'sphinxcontrib-asyncio~=0.3.0',
52+
'sphinx_rtd_theme~=0.5.2',
53+
]
54+
55+
[build-system]
56+
requires = [
57+
"setuptools>=60",
58+
"wheel",
59+
"Cython(>=0.29.36,<0.30.0)",
60+
]
61+
build-backend = "setuptools.build_meta"
62+
63+
[tool.setuptools]
64+
zip-safe = false
65+
packages = ["uvloop"]
66+
67+
[tool.cibuildwheel]
68+
build-frontend = "build"
69+
test-extras = "test"
70+
test-command = "python -m unittest discover -v {project}/tests"
71+
72+
[tool.pytest.ini_options]
73+
addopts = "--capture=no --assert=plain --strict-markers --tb=native --import-mode=importlib"
74+
testpaths = "tests"
75+
filterwarnings = "default"

pytest.ini

Lines changed: 0 additions & 4 deletions
This file was deleted.

setup.py

Lines changed: 1 addition & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -21,40 +21,7 @@
2121
from setuptools.command.sdist import sdist
2222

2323

24-
CYTHON_DEPENDENCY = 'Cython(>=0.29.32,<0.30.0)'
25-
26-
# Minimal dependencies required to test uvloop.
27-
TEST_DEPENDENCIES = [
28-
# pycodestyle is a dependency of flake8, but it must be frozen because
29-
# their combination breaks too often
30-
# (example breakage: https://gitlab.com/pycqa/flake8/issues/427)
31-
'aiohttp>=3.8.1',
32-
'flake8~=5.0',
33-
'psutil',
34-
'pycodestyle~=2.9.0',
35-
'pyOpenSSL~=23.0.0',
36-
'mypy>=0.800',
37-
CYTHON_DEPENDENCY,
38-
]
39-
40-
# Dependencies required to build documentation.
41-
DOC_DEPENDENCIES = [
42-
'Sphinx~=4.1.2',
43-
'sphinxcontrib-asyncio~=0.3.0',
44-
'sphinx_rtd_theme~=0.5.2',
45-
]
46-
47-
EXTRA_DEPENDENCIES = {
48-
'docs': DOC_DEPENDENCIES,
49-
'test': TEST_DEPENDENCIES,
50-
# Dependencies required to develop uvloop.
51-
'dev': [
52-
CYTHON_DEPENDENCY,
53-
'pytest>=3.6.0',
54-
] + DOC_DEPENDENCIES + TEST_DEPENDENCIES
55-
}
56-
57-
24+
CYTHON_DEPENDENCY = 'Cython(>=0.29.36,<0.30.0)'
5825
MACHINE = platform.machine()
5926
MODULES_CFLAGS = [os.getenv('UVLOOP_OPT_CFLAGS', '-O2')]
6027
_ROOT = pathlib.Path(__file__).parent
@@ -245,10 +212,6 @@ def build_extensions(self):
245212
super().build_extensions()
246213

247214

248-
with open(str(_ROOT / 'README.rst')) as f:
249-
readme = f.read()
250-
251-
252215
with open(str(_ROOT / 'uvloop' / '_version.py')) as f:
253216
for line in f:
254217
if line.startswith('__version__ ='):
@@ -268,16 +231,7 @@ def build_extensions(self):
268231

269232

270233
setup(
271-
name='uvloop',
272-
description='Fast implementation of asyncio event loop on top of libuv',
273-
long_description=readme,
274-
url='http://github.com/MagicStack/uvloop',
275-
license='MIT',
276-
author='Yury Selivanov',
277-
author_email='[email protected]',
278-
platforms=['macOS', 'POSIX'],
279234
version=VERSION,
280-
packages=['uvloop'],
281235
cmdclass={
282236
'sdist': uvloop_sdist,
283237
'build_ext': uvloop_build_ext
@@ -291,20 +245,5 @@ def build_extensions(self):
291245
extra_compile_args=MODULES_CFLAGS
292246
),
293247
],
294-
classifiers=[
295-
'Development Status :: 5 - Production/Stable',
296-
'Framework :: AsyncIO',
297-
'Programming Language :: Python :: 3 :: Only',
298-
'Programming Language :: Python :: 3.7',
299-
'Programming Language :: Python :: 3.8',
300-
'Programming Language :: Python :: 3.9',
301-
'Programming Language :: Python :: 3.10',
302-
'License :: OSI Approved :: Apache Software License',
303-
'License :: OSI Approved :: MIT License',
304-
'Intended Audience :: Developers',
305-
],
306-
include_package_data=True,
307-
extras_require=EXTRA_DEPENDENCIES,
308248
setup_requires=setup_requires,
309-
python_requires='>=3.7',
310249
)

tests/cython_helper.pyx

Lines changed: 0 additions & 5 deletions
This file was deleted.

tests/test_aiohttp.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
skip_tests = False
88

99
import asyncio
10+
import sys
1011
import unittest
1112
import weakref
1213

@@ -48,6 +49,14 @@ async def test():
4849
self.loop.run_until_complete(runner.cleanup())
4950

5051
def test_aiohttp_graceful_shutdown(self):
52+
if self.implementation == 'asyncio' and sys.version_info >= (3, 12, 0):
53+
# In Python 3.12.0, asyncio.Server.wait_closed() waits for all
54+
# existing connections to complete, before aiohttp sends
55+
# on_shutdown signals.
56+
# https://github.com/aio-libs/aiohttp/issues/7675#issuecomment-1752143748
57+
# https://github.com/python/cpython/pull/98582
58+
raise unittest.SkipTest('bug in aiohttp: #7675')
59+
5160
async def websocket_handler(request):
5261
ws = aiohttp.web.WebSocketResponse()
5362
await ws.prepare(request)
@@ -73,7 +82,13 @@ async def on_shutdown(app):
7382

7483
runner = aiohttp.web.AppRunner(app)
7584
self.loop.run_until_complete(runner.setup())
76-
site = aiohttp.web.TCPSite(runner, '0.0.0.0', '0')
85+
site = aiohttp.web.TCPSite(
86+
runner,
87+
'0.0.0.0',
88+
0,
89+
# https://github.com/aio-libs/aiohttp/pull/7188
90+
shutdown_timeout=0.1,
91+
)
7792
self.loop.run_until_complete(site.start())
7893
port = site._server.sockets[0].getsockname()[1]
7994

@@ -90,7 +105,7 @@ async def client():
90105
async def stop():
91106
await asyncio.sleep(0.1)
92107
try:
93-
await asyncio.wait_for(runner.cleanup(), timeout=0.1)
108+
await asyncio.wait_for(runner.cleanup(), timeout=0.5)
94109
finally:
95110
try:
96111
client_task.cancel()

tests/test_dealloc.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,12 @@ def test_dealloc_1(self):
3030
async def test():
3131
prog = '''\
3232
import uvloop
33-
import asyncio
3433
3534
async def foo():
3635
return 42
3736
3837
def main():
39-
uvloop.install()
40-
loop = asyncio.get_event_loop()
38+
loop = uvloop.new_event_loop()
4139
loop.set_debug(True)
4240
loop.run_until_complete(foo())
4341
# Do not close the loop on purpose: let __dealloc__ methods run.

tests/test_libuv_api.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
from uvloop import _testbase as tb
22
from uvloop.loop import libuv_get_loop_t_ptr, libuv_get_version
3+
from uvloop.loop import _testhelper_unwrap_capsuled_pointer as unwrap
34

45

56
class Test_UV_libuv(tb.UVTestCase):
67
def test_libuv_get_loop_t_ptr(self):
7-
loop = self.new_loop()
8-
cap1 = libuv_get_loop_t_ptr(loop)
9-
cap2 = libuv_get_loop_t_ptr(loop)
10-
cap3 = libuv_get_loop_t_ptr(self.new_loop())
8+
loop1 = self.new_loop()
9+
cap1 = libuv_get_loop_t_ptr(loop1)
10+
cap2 = libuv_get_loop_t_ptr(loop1)
1111

12-
import pyximport
12+
loop2 = self.new_loop()
13+
cap3 = libuv_get_loop_t_ptr(loop2)
1314

14-
pyximport.install()
15-
16-
import cython_helper
17-
18-
self.assertTrue(cython_helper.capsule_equals(cap1, cap2))
19-
self.assertFalse(cython_helper.capsule_equals(cap1, cap3))
15+
try:
16+
self.assertEqual(unwrap(cap1), unwrap(cap2))
17+
self.assertNotEqual(unwrap(cap1), unwrap(cap3))
18+
finally:
19+
loop1.close()
20+
loop2.close()
2021

2122
def test_libuv_get_version(self):
2223
self.assertGreater(libuv_get_version(), 0)

tests/test_process.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -851,8 +851,7 @@ async def test():
851851
)
852852
await proc.communicate()
853853
854-
uvloop.install()
855-
asyncio.run(test())
854+
uvloop.run(test())
856855
857856
stdin, stdout, stderr = dups
858857
(r, w), = pipes

tests/test_runner.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import asyncio
2+
import unittest
3+
import uvloop
4+
5+
6+
class TestSourceCode(unittest.TestCase):
7+
8+
def test_uvloop_run_1(self):
9+
CNT = 0
10+
11+
async def main():
12+
nonlocal CNT
13+
CNT += 1
14+
15+
loop = asyncio.get_running_loop()
16+
17+
self.assertTrue(isinstance(loop, uvloop.Loop))
18+
self.assertTrue(loop.get_debug())
19+
20+
return 'done'
21+
22+
result = uvloop.run(main(), debug=True)
23+
24+
self.assertEqual(result, 'done')
25+
self.assertEqual(CNT, 1)
26+
27+
def test_uvloop_run_2(self):
28+
29+
async def main():
30+
pass
31+
32+
coro = main()
33+
with self.assertRaisesRegex(TypeError, ' a non-uvloop event loop'):
34+
uvloop.run(
35+
coro,
36+
loop_factory=asyncio.DefaultEventLoopPolicy().new_event_loop,
37+
)
38+
39+
coro.close()

0 commit comments

Comments
 (0)