diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ec865560..d345aef2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -75,7 +75,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - cibw_python: ["cp37-*", "cp38-*", "cp39-*", "cp310-*", "cp311-*"] + cibw_python: ["cp37-*", "cp38-*", "cp39-*", "cp310-*", "cp311-*", "cp312-*"] cibw_arch: ["x86_64", "aarch64", "universal2"] exclude: - os: ubuntu-latest @@ -110,14 +110,11 @@ jobs: run: | brew install gnu-sed libtool autoconf automake - - uses: pypa/cibuildwheel@v2.9.0 + - uses: pypa/cibuildwheel@v2.16.2 env: CIBW_BUILD_VERBOSITY: 1 CIBW_BUILD: ${{ matrix.cibw_python }} CIBW_ARCHS: ${{ matrix.cibw_arch }} - CIBW_TEST_EXTRAS: "test" - CIBW_TEST_COMMAND: "python -m unittest discover -v {project}/tests" - CIBW_TEST_COMMAND_WINDOWS: "python -m unittest discover -v {project}\\tests" CIBW_TEST_SKIP: "*universal2:arm64" - uses: actions/upload-artifact@v3 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0fdcb62b..3aee2a99 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] os: [ubuntu-latest, macos-latest] env: @@ -58,7 +58,8 @@ jobs: make test - name: Test (debug build) - if: steps.release.outputs.version == 0 + # XXX Re-enable 3.12 once we migrate to Cython 3 + if: steps.release.outputs.version == 0 && matrix.python-version != '3.12' run: | make distclean && make debug && make test diff --git a/README.rst b/README.rst index 1464fe7e..a0c0239a 100644 --- a/README.rst +++ b/README.rst @@ -53,6 +53,27 @@ uvloop with:: Using uvloop ------------ +As of uvloop 0.18, the preferred way of using it is via the +``uvloop.run()`` helper function: + + +.. code:: python + + import uvloop + + async def main(): + # Main entry-point. + ... + + uvloop.run(main()) + +``uvloop.run()`` works by simply configuring ``asyncio.run()`` +to use uvloop, passing all of the arguments to it, such as ``debug``, +e.g. ``uvloop.run(main(), debug=True)``. + +With Python 3.11 and earlier the following alternative +snippet can be used: + .. code:: python import asyncio diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..0d6d472a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,75 @@ +[project] +name = "uvloop" +description = "Fast implementation of asyncio event loop on top of libuv" +authors = [{name = "Yury Selivanov", email = "yury@magic.io"}] +requires-python = '>=3.7.0' +readme = "README.rst" +license = {text = "MIT License"} +dynamic = ["version"] +keywords = [ + "asyncio", + "networking", +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Framework :: AsyncIO", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "License :: OSI Approved :: MIT License", + "Operating System :: POSIX", + "Operating System :: MacOS :: MacOS X", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: System :: Networking", +] + +[project.urls] +github = "https://github.com/MagicStack/uvloop" + +[project.optional-dependencies] +test = [ + # pycodestyle is a dependency of flake8, but it must be frozen because + # their combination breaks too often + # (example breakage: https://gitlab.com/pycqa/flake8/issues/427) + 'aiohttp>=3.8.1; python_version < "3.12"', + 'aiohttp==3.9.0b0; python_version >= "3.12"', + 'flake8~=5.0', + 'psutil', + 'pycodestyle~=2.9.0', + 'pyOpenSSL~=23.0.0', + 'mypy>=0.800', + 'Cython(>=0.29.36,<0.30.0)', +] +docs = [ + 'Sphinx~=4.1.2', + 'sphinxcontrib-asyncio~=0.3.0', + 'sphinx_rtd_theme~=0.5.2', +] + +[build-system] +requires = [ + "setuptools>=60", + "wheel", + "Cython(>=0.29.36,<0.30.0)", +] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +zip-safe = false +packages = ["uvloop"] + +[tool.cibuildwheel] +build-frontend = "build" +test-extras = "test" +test-command = "python -m unittest discover -v {project}/tests" + +[tool.pytest.ini_options] +addopts = "--capture=no --assert=plain --strict-markers --tb=native --import-mode=importlib" +testpaths = "tests" +filterwarnings = "default" diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index fcfb4455..00000000 --- a/pytest.ini +++ /dev/null @@ -1,4 +0,0 @@ -[pytest] -addopts = --capture=no --assert=plain --strict-markers --tb native -testpaths = tests -filterwarnings = default diff --git a/setup.py b/setup.py index 15604cde..5afaee97 100644 --- a/setup.py +++ b/setup.py @@ -21,40 +21,7 @@ from setuptools.command.sdist import sdist -CYTHON_DEPENDENCY = 'Cython(>=0.29.32,<0.30.0)' - -# Minimal dependencies required to test uvloop. -TEST_DEPENDENCIES = [ - # pycodestyle is a dependency of flake8, but it must be frozen because - # their combination breaks too often - # (example breakage: https://gitlab.com/pycqa/flake8/issues/427) - 'aiohttp>=3.8.1', - 'flake8~=5.0', - 'psutil', - 'pycodestyle~=2.9.0', - 'pyOpenSSL~=23.0.0', - 'mypy>=0.800', - CYTHON_DEPENDENCY, -] - -# Dependencies required to build documentation. -DOC_DEPENDENCIES = [ - 'Sphinx~=4.1.2', - 'sphinxcontrib-asyncio~=0.3.0', - 'sphinx_rtd_theme~=0.5.2', -] - -EXTRA_DEPENDENCIES = { - 'docs': DOC_DEPENDENCIES, - 'test': TEST_DEPENDENCIES, - # Dependencies required to develop uvloop. - 'dev': [ - CYTHON_DEPENDENCY, - 'pytest>=3.6.0', - ] + DOC_DEPENDENCIES + TEST_DEPENDENCIES -} - - +CYTHON_DEPENDENCY = 'Cython(>=0.29.36,<0.30.0)' MACHINE = platform.machine() MODULES_CFLAGS = [os.getenv('UVLOOP_OPT_CFLAGS', '-O2')] _ROOT = pathlib.Path(__file__).parent @@ -245,10 +212,6 @@ def build_extensions(self): super().build_extensions() -with open(str(_ROOT / 'README.rst')) as f: - readme = f.read() - - with open(str(_ROOT / 'uvloop' / '_version.py')) as f: for line in f: if line.startswith('__version__ ='): @@ -268,16 +231,7 @@ def build_extensions(self): setup( - name='uvloop', - description='Fast implementation of asyncio event loop on top of libuv', - long_description=readme, - url='http://github.com/MagicStack/uvloop', - license='MIT', - author='Yury Selivanov', - author_email='yury@magic.io', - platforms=['macOS', 'POSIX'], version=VERSION, - packages=['uvloop'], cmdclass={ 'sdist': uvloop_sdist, 'build_ext': uvloop_build_ext @@ -291,20 +245,5 @@ def build_extensions(self): extra_compile_args=MODULES_CFLAGS ), ], - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Framework :: AsyncIO', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'License :: OSI Approved :: Apache Software License', - 'License :: OSI Approved :: MIT License', - 'Intended Audience :: Developers', - ], - include_package_data=True, - extras_require=EXTRA_DEPENDENCIES, setup_requires=setup_requires, - python_requires='>=3.7', ) diff --git a/tests/cython_helper.pyx b/tests/cython_helper.pyx deleted file mode 100644 index 05c691da..00000000 --- a/tests/cython_helper.pyx +++ /dev/null @@ -1,5 +0,0 @@ -from cpython.pycapsule cimport PyCapsule_GetPointer - - -def capsule_equals(cap1, cap2): - return PyCapsule_GetPointer(cap1, NULL) == PyCapsule_GetPointer(cap2, NULL) diff --git a/tests/test_aiohttp.py b/tests/test_aiohttp.py index cdf8717b..514d0177 100644 --- a/tests/test_aiohttp.py +++ b/tests/test_aiohttp.py @@ -7,6 +7,7 @@ skip_tests = False import asyncio +import sys import unittest import weakref @@ -48,6 +49,14 @@ async def test(): self.loop.run_until_complete(runner.cleanup()) def test_aiohttp_graceful_shutdown(self): + if self.implementation == 'asyncio' and sys.version_info >= (3, 12, 0): + # In Python 3.12.0, asyncio.Server.wait_closed() waits for all + # existing connections to complete, before aiohttp sends + # on_shutdown signals. + # https://github.com/aio-libs/aiohttp/issues/7675#issuecomment-1752143748 + # https://github.com/python/cpython/pull/98582 + raise unittest.SkipTest('bug in aiohttp: #7675') + async def websocket_handler(request): ws = aiohttp.web.WebSocketResponse() await ws.prepare(request) @@ -73,7 +82,13 @@ async def on_shutdown(app): runner = aiohttp.web.AppRunner(app) self.loop.run_until_complete(runner.setup()) - site = aiohttp.web.TCPSite(runner, '0.0.0.0', '0') + site = aiohttp.web.TCPSite( + runner, + '0.0.0.0', + 0, + # https://github.com/aio-libs/aiohttp/pull/7188 + shutdown_timeout=0.1, + ) self.loop.run_until_complete(site.start()) port = site._server.sockets[0].getsockname()[1] @@ -90,7 +105,7 @@ async def client(): async def stop(): await asyncio.sleep(0.1) try: - await asyncio.wait_for(runner.cleanup(), timeout=0.1) + await asyncio.wait_for(runner.cleanup(), timeout=0.5) finally: try: client_task.cancel() diff --git a/tests/test_dealloc.py b/tests/test_dealloc.py index 0b9b2f6e..66500a18 100644 --- a/tests/test_dealloc.py +++ b/tests/test_dealloc.py @@ -30,14 +30,12 @@ def test_dealloc_1(self): async def test(): prog = '''\ import uvloop -import asyncio async def foo(): return 42 def main(): - uvloop.install() - loop = asyncio.get_event_loop() + loop = uvloop.new_event_loop() loop.set_debug(True) loop.run_until_complete(foo()) # Do not close the loop on purpose: let __dealloc__ methods run. diff --git a/tests/test_libuv_api.py b/tests/test_libuv_api.py index 3cda4bcd..1683952f 100644 --- a/tests/test_libuv_api.py +++ b/tests/test_libuv_api.py @@ -1,22 +1,23 @@ from uvloop import _testbase as tb from uvloop.loop import libuv_get_loop_t_ptr, libuv_get_version +from uvloop.loop import _testhelper_unwrap_capsuled_pointer as unwrap class Test_UV_libuv(tb.UVTestCase): def test_libuv_get_loop_t_ptr(self): - loop = self.new_loop() - cap1 = libuv_get_loop_t_ptr(loop) - cap2 = libuv_get_loop_t_ptr(loop) - cap3 = libuv_get_loop_t_ptr(self.new_loop()) + loop1 = self.new_loop() + cap1 = libuv_get_loop_t_ptr(loop1) + cap2 = libuv_get_loop_t_ptr(loop1) - import pyximport + loop2 = self.new_loop() + cap3 = libuv_get_loop_t_ptr(loop2) - pyximport.install() - - import cython_helper - - self.assertTrue(cython_helper.capsule_equals(cap1, cap2)) - self.assertFalse(cython_helper.capsule_equals(cap1, cap3)) + try: + self.assertEqual(unwrap(cap1), unwrap(cap2)) + self.assertNotEqual(unwrap(cap1), unwrap(cap3)) + finally: + loop1.close() + loop2.close() def test_libuv_get_version(self): self.assertGreater(libuv_get_version(), 0) diff --git a/tests/test_process.py b/tests/test_process.py index 13585189..ade4742c 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -851,8 +851,7 @@ async def test(): ) await proc.communicate() - uvloop.install() - asyncio.run(test()) + uvloop.run(test()) stdin, stdout, stderr = dups (r, w), = pipes diff --git a/tests/test_runner.py b/tests/test_runner.py new file mode 100644 index 00000000..cb672cec --- /dev/null +++ b/tests/test_runner.py @@ -0,0 +1,39 @@ +import asyncio +import unittest +import uvloop + + +class TestSourceCode(unittest.TestCase): + + def test_uvloop_run_1(self): + CNT = 0 + + async def main(): + nonlocal CNT + CNT += 1 + + loop = asyncio.get_running_loop() + + self.assertTrue(isinstance(loop, uvloop.Loop)) + self.assertTrue(loop.get_debug()) + + return 'done' + + result = uvloop.run(main(), debug=True) + + self.assertEqual(result, 'done') + self.assertEqual(CNT, 1) + + def test_uvloop_run_2(self): + + async def main(): + pass + + coro = main() + with self.assertRaisesRegex(TypeError, ' a non-uvloop event loop'): + uvloop.run( + coro, + loop_factory=asyncio.DefaultEventLoopPolicy().new_event_loop, + ) + + coro.close() diff --git a/tests/test_tcp.py b/tests/test_tcp.py index a5954355..51f45664 100644 --- a/tests/test_tcp.py +++ b/tests/test_tcp.py @@ -133,6 +133,15 @@ async def start_server(): self.loop.call_soon(srv.close) await srv.wait_closed() + if ( + self.implementation == 'asyncio' + and sys.version_info[:3] >= (3, 12, 0) + ): + # asyncio regression in 3.12 -- wait_closed() + # doesn't wait for `close()` to actually complete. + # https://github.com/python/cpython/issues/79033 + await asyncio.sleep(1) + # Check that the server cleaned-up proxy-sockets for srv_sock in srv_socks: self.assertEqual(srv_sock.fileno(), -1) @@ -168,6 +177,15 @@ async def start_server_sock(): srv.close() await srv.wait_closed() + if ( + self.implementation == 'asyncio' + and sys.version_info[:3] >= (3, 12, 0) + ): + # asyncio regression in 3.12 -- wait_closed() + # doesn't wait for `close()` to actually complete. + # https://github.com/python/cpython/issues/79033 + await asyncio.sleep(1) + # Check that the server cleaned-up proxy-sockets for srv_sock in srv_socks: self.assertEqual(srv_sock.fileno(), -1) @@ -205,6 +223,15 @@ async def start_server_ephemeral_ports(): self.loop.call_soon(srv.close) await srv.wait_closed() + if ( + self.implementation == 'asyncio' + and sys.version_info[:3] >= (3, 12, 0) + ): + # asyncio regression in 3.12 -- wait_closed() + # doesn't wait for `close()` to actually complete. + # https://github.com/python/cpython/issues/79033 + await asyncio.sleep(1) + # Check that the server cleaned-up proxy-sockets for srv_sock in srv_socks: self.assertEqual(srv_sock.fileno(), -1) diff --git a/tests/test_unix.py b/tests/test_unix.py index 87c1e7e1..8adedeef 100644 --- a/tests/test_unix.py +++ b/tests/test_unix.py @@ -81,6 +81,15 @@ async def start_server(): self.loop.call_soon(srv.close) await srv.wait_closed() + if ( + self.implementation == 'asyncio' + and sys.version_info[:3] >= (3, 12, 0) + ): + # asyncio regression in 3.12 -- wait_closed() + # doesn't wait for `close()` to actually complete. + # https://github.com/python/cpython/issues/79033 + await asyncio.sleep(1) + # Check that the server cleaned-up proxy-sockets for srv_sock in srv_socks: self.assertEqual(srv_sock.fileno(), -1) @@ -116,6 +125,15 @@ async def start_server_sock(start_server): self.loop.call_soon(srv.close) await srv.wait_closed() + if ( + self.implementation == 'asyncio' + and sys.version_info[:3] >= (3, 12, 0) + ): + # asyncio regression in 3.12 -- wait_closed() + # doesn't wait for `close()` to actually complete. + # https://github.com/python/cpython/issues/79033 + await asyncio.sleep(1) + # Check that the server cleaned-up proxy-sockets for srv_sock in srv_socks: self.assertEqual(srv_sock.fileno(), -1) diff --git a/uvloop/__init__.py b/uvloop/__init__.py index 529b0a99..1fa158a6 100644 --- a/uvloop/__init__.py +++ b/uvloop/__init__.py @@ -1,5 +1,7 @@ import asyncio as __asyncio import typing as _typing +import sys as _sys +import warnings as _warnings from asyncio.events import BaseDefaultEventLoopPolicy as __BasePolicy @@ -22,9 +24,103 @@ def new_event_loop() -> Loop: def install() -> None: """A helper function to install uvloop policy.""" + if _sys.version_info[:2] >= (3, 12): + _warnings.warn( + 'uvloop.install() is deprecated in favor of uvloop.run() ' + 'starting with Python 3.12.', + DeprecationWarning, + stacklevel=1, + ) __asyncio.set_event_loop_policy(EventLoopPolicy()) +def run(main, *, loop_factory=new_event_loop, debug=None, **run_kwargs): + """The preferred way of running a coroutine with uvloop.""" + + async def wrapper(): + # If `loop_factory` is provided we want it to return + # either uvloop.Loop or a subtype of it, assuming the user + # is using `uvloop.run()` intentionally. + loop = __asyncio._get_running_loop() + if not isinstance(loop, Loop): + raise TypeError('uvloop.run() uses a non-uvloop event loop') + return await main + + vi = _sys.version_info[:2] + + if vi <= (3, 10): + # Copied from python/cpython + + if __asyncio._get_running_loop() is not None: + raise RuntimeError( + "asyncio.run() cannot be called from a running event loop") + + if not __asyncio.iscoroutine(main): + raise ValueError("a coroutine was expected, got {!r}".format(main)) + + loop = loop_factory() + try: + __asyncio.set_event_loop(loop) + if debug is not None: + loop.set_debug(debug) + return loop.run_until_complete(wrapper()) + finally: + try: + _cancel_all_tasks(loop) + loop.run_until_complete(loop.shutdown_asyncgens()) + if hasattr(loop, 'shutdown_default_executor'): + loop.run_until_complete(loop.shutdown_default_executor()) + finally: + __asyncio.set_event_loop(None) + loop.close() + + elif vi == (3, 11): + if __asyncio._get_running_loop() is not None: + raise RuntimeError( + "asyncio.run() cannot be called from a running event loop") + + with __asyncio.Runner( + loop_factory=loop_factory, + debug=debug, + **run_kwargs + ) as runner: + return runner.run(wrapper()) + + else: + assert vi >= (3, 12) + return __asyncio.run( + wrapper(), + loop_factory=loop_factory, + debug=debug, + **run_kwargs + ) + + +def _cancel_all_tasks(loop): + # Copied from python/cpython + + to_cancel = __asyncio.all_tasks(loop) + if not to_cancel: + return + + for task in to_cancel: + task.cancel() + + loop.run_until_complete( + __asyncio.gather(*to_cancel, return_exceptions=True) + ) + + for task in to_cancel: + if task.cancelled(): + continue + if task.exception() is not None: + loop.call_exception_handler({ + 'message': 'unhandled exception during asyncio.run() shutdown', + 'exception': task.exception(), + 'task': task, + }) + + class EventLoopPolicy(__BasePolicy): """Event loop policy. diff --git a/uvloop/__init__.pyi b/uvloop/__init__.pyi new file mode 100644 index 00000000..06194d47 --- /dev/null +++ b/uvloop/__init__.pyi @@ -0,0 +1,17 @@ +import sys + +from asyncio import AbstractEventLoop +from collections.abc import Callable, Coroutine +from contextvars import Context +from typing import Any, TypeVar + + +_T = TypeVar('_T') + + +def run( + main: Coroutine[Any, Any, _T], + *, + debug: bool | None = ..., + loop_factory: Callable[[], AbstractEventLoop] | None = ..., +) -> _T: ... diff --git a/uvloop/_testbase.py b/uvloop/_testbase.py index 3cc828cb..c4a7595b 100644 --- a/uvloop/_testbase.py +++ b/uvloop/_testbase.py @@ -13,6 +13,7 @@ import select import socket import ssl +import sys import tempfile import threading import time @@ -313,12 +314,14 @@ class AIOTestCase(BaseTestCase): def setUp(self): super().setUp() - watcher = asyncio.SafeChildWatcher() - watcher.attach_loop(self.loop) - asyncio.set_child_watcher(watcher) + if sys.version_info < (3, 12): + watcher = asyncio.SafeChildWatcher() + watcher.attach_loop(self.loop) + asyncio.set_child_watcher(watcher) def tearDown(self): - asyncio.set_child_watcher(None) + if sys.version_info < (3, 12): + asyncio.set_child_watcher(None) super().tearDown() def new_loop(self): diff --git a/uvloop/dns.pyx b/uvloop/dns.pyx index e15e65d1..7aad6319 100644 --- a/uvloop/dns.pyx +++ b/uvloop/dns.pyx @@ -414,8 +414,11 @@ cdef _intenum_converter(value, enum_klass): return value -cdef void __on_addrinfo_resolved(uv.uv_getaddrinfo_t *resolver, - int status, system.addrinfo *res) with gil: +cdef void __on_addrinfo_resolved( + uv.uv_getaddrinfo_t *resolver, + int status, + system.addrinfo *res, +) noexcept with gil: if resolver.data is NULL: aio_logger.error( @@ -443,10 +446,12 @@ cdef void __on_addrinfo_resolved(uv.uv_getaddrinfo_t *resolver, request.on_done() -cdef void __on_nameinfo_resolved(uv.uv_getnameinfo_t* req, - int status, - const char* hostname, - const char* service) with gil: +cdef void __on_nameinfo_resolved( + uv.uv_getnameinfo_t* req, + int status, + const char* hostname, + const char* service, +) noexcept with gil: cdef: NameInfoRequest request = req.data Loop loop = request.loop diff --git a/uvloop/handles/async_.pyx b/uvloop/handles/async_.pyx index 7c84389a..5c740cfe 100644 --- a/uvloop/handles/async_.pyx +++ b/uvloop/handles/async_.pyx @@ -41,7 +41,9 @@ cdef class UVAsync(UVHandle): return handle -cdef void __uvasync_callback(uv.uv_async_t* handle) with gil: +cdef void __uvasync_callback( + uv.uv_async_t* handle, +) noexcept with gil: if __ensure_handle_data(handle, "UVAsync callback") == 0: return diff --git a/uvloop/handles/check.pyx b/uvloop/handles/check.pyx index a8d6f92a..1a61c4e9 100644 --- a/uvloop/handles/check.pyx +++ b/uvloop/handles/check.pyx @@ -57,7 +57,9 @@ cdef class UVCheck(UVHandle): return handle -cdef void cb_check_callback(uv.uv_check_t* handle) with gil: +cdef void cb_check_callback( + uv.uv_check_t* handle, +) noexcept with gil: if __ensure_handle_data(handle, "UVCheck callback") == 0: return diff --git a/uvloop/handles/fsevent.pyx b/uvloop/handles/fsevent.pyx index 7b458c29..6ed6433c 100644 --- a/uvloop/handles/fsevent.pyx +++ b/uvloop/handles/fsevent.pyx @@ -89,8 +89,12 @@ cdef class UVFSEvent(UVHandle): return handle -cdef void __uvfsevent_callback(uv.uv_fs_event_t* handle, const char *filename, - int events, int status) with gil: +cdef void __uvfsevent_callback( + uv.uv_fs_event_t* handle, + const char *filename, + int events, + int status, +) noexcept with gil: if __ensure_handle_data( handle, "UVFSEvent callback" ) == 0: diff --git a/uvloop/handles/handle.pyx b/uvloop/handles/handle.pyx index c3c691fe..6efe3755 100644 --- a/uvloop/handles/handle.pyx +++ b/uvloop/handles/handle.pyx @@ -335,7 +335,7 @@ cdef inline bint __ensure_handle_data(uv.uv_handle_t* handle, return 1 -cdef void __uv_close_handle_cb(uv.uv_handle_t* handle) with gil: +cdef void __uv_close_handle_cb(uv.uv_handle_t* handle) noexcept with gil: cdef UVHandle h if handle.data is NULL: @@ -370,7 +370,9 @@ cdef void __close_all_handles(Loop loop): cdef void __uv_walk_close_all_handles_cb( - uv.uv_handle_t* handle, void* arg) with gil: + uv.uv_handle_t* handle, + void* arg, +) noexcept with gil: cdef: Loop loop = arg diff --git a/uvloop/handles/idle.pyx b/uvloop/handles/idle.pyx index ca782190..91c641f6 100644 --- a/uvloop/handles/idle.pyx +++ b/uvloop/handles/idle.pyx @@ -57,7 +57,9 @@ cdef class UVIdle(UVHandle): return handle -cdef void cb_idle_callback(uv.uv_idle_t* handle) with gil: +cdef void cb_idle_callback( + uv.uv_idle_t* handle, +) noexcept with gil: if __ensure_handle_data(handle, "UVIdle callback") == 0: return diff --git a/uvloop/handles/pipe.pyx b/uvloop/handles/pipe.pyx index 19dc3bd5..195576c7 100644 --- a/uvloop/handles/pipe.pyx +++ b/uvloop/handles/pipe.pyx @@ -202,7 +202,10 @@ cdef class _PipeConnectRequest(UVRequest): addr, __pipe_connect_callback) -cdef void __pipe_connect_callback(uv.uv_connect_t* req, int status) with gil: +cdef void __pipe_connect_callback( + uv.uv_connect_t* req, + int status, +) noexcept with gil: cdef: _PipeConnectRequest wrapper UnixTransport transport diff --git a/uvloop/handles/poll.pyx b/uvloop/handles/poll.pyx index ebdbfd89..fca5981e 100644 --- a/uvloop/handles/poll.pyx +++ b/uvloop/handles/poll.pyx @@ -190,8 +190,11 @@ cdef class UVPoll(UVHandle): self._close() -cdef void __on_uvpoll_event(uv.uv_poll_t* handle, - int status, int events) with gil: +cdef void __on_uvpoll_event( + uv.uv_poll_t* handle, + int status, + int events, +) noexcept with gil: if __ensure_handle_data(handle, "UVPoll callback") == 0: return diff --git a/uvloop/handles/process.pyx b/uvloop/handles/process.pyx index 1ffbd8ca..89208357 100644 --- a/uvloop/handles/process.pyx +++ b/uvloop/handles/process.pyx @@ -730,9 +730,11 @@ cdef __process_convert_fileno(object obj): return fileno -cdef void __uvprocess_on_exit_callback(uv.uv_process_t *handle, - int64_t exit_status, - int term_signal) with gil: +cdef void __uvprocess_on_exit_callback( + uv.uv_process_t *handle, + int64_t exit_status, + int term_signal, +) noexcept with gil: if __ensure_handle_data(handle, "UVProcess exit callback") == 0: @@ -761,5 +763,7 @@ cdef __socketpair(): return fds[0], fds[1] -cdef void __uv_close_process_handle_cb(uv.uv_handle_t* handle) with gil: +cdef void __uv_close_process_handle_cb( + uv.uv_handle_t* handle +) noexcept with gil: PyMem_RawFree(handle) diff --git a/uvloop/handles/stream.pyx b/uvloop/handles/stream.pyx index 6ce096d8..d4e02e3e 100644 --- a/uvloop/handles/stream.pyx +++ b/uvloop/handles/stream.pyx @@ -723,7 +723,7 @@ cdef class UVStream(UVBaseTransport): cdef void __uv_stream_on_shutdown(uv.uv_shutdown_t* req, - int status) with gil: + int status) noexcept with gil: # callback for uv_shutdown @@ -751,8 +751,11 @@ cdef void __uv_stream_on_shutdown(uv.uv_shutdown_t* req, return -cdef inline bint __uv_stream_on_read_common(UVStream sc, Loop loop, - ssize_t nread): +cdef inline bint __uv_stream_on_read_common( + UVStream sc, + Loop loop, + ssize_t nread, +): if sc._closed: # The stream was closed, there is no reason to # do any work now. @@ -811,9 +814,11 @@ cdef inline bint __uv_stream_on_read_common(UVStream sc, Loop loop, return False -cdef inline void __uv_stream_on_read_impl(uv.uv_stream_t* stream, - ssize_t nread, - const uv.uv_buf_t* buf): +cdef inline void __uv_stream_on_read_impl( + uv.uv_stream_t* stream, + ssize_t nread, + const uv.uv_buf_t* buf, +): cdef: UVStream sc = stream.data Loop loop = sc._loop @@ -841,7 +846,10 @@ cdef inline void __uv_stream_on_read_impl(uv.uv_stream_t* stream, sc._fatal_error(exc, False) -cdef inline void __uv_stream_on_write_impl(uv.uv_write_t* req, int status): +cdef inline void __uv_stream_on_write_impl( + uv.uv_write_t* req, + int status, +): cdef: _StreamWriteContext ctx = <_StreamWriteContext> req.data UVStream stream = ctx.stream @@ -872,9 +880,11 @@ cdef inline void __uv_stream_on_write_impl(uv.uv_write_t* req, int status): stream._fatal_error(exc, False) -cdef void __uv_stream_on_read(uv.uv_stream_t* stream, - ssize_t nread, - const uv.uv_buf_t* buf) with gil: +cdef void __uv_stream_on_read( + uv.uv_stream_t* stream, + ssize_t nread, + const uv.uv_buf_t* buf, +) noexcept with gil: if __ensure_handle_data(stream, "UVStream read callback") == 0: @@ -884,7 +894,10 @@ cdef void __uv_stream_on_read(uv.uv_stream_t* stream, __uv_stream_on_read_impl(stream, nread, buf) -cdef void __uv_stream_on_write(uv.uv_write_t* req, int status) with gil: +cdef void __uv_stream_on_write( + uv.uv_write_t* req, + int status, +) noexcept with gil: if UVLOOP_DEBUG: if req.data is NULL: @@ -897,9 +910,11 @@ cdef void __uv_stream_on_write(uv.uv_write_t* req, int status) with gil: __uv_stream_on_write_impl(req, status) -cdef void __uv_stream_buffered_alloc(uv.uv_handle_t* stream, - size_t suggested_size, - uv.uv_buf_t* uvbuf) with gil: +cdef void __uv_stream_buffered_alloc( + uv.uv_handle_t* stream, + size_t suggested_size, + uv.uv_buf_t* uvbuf, +) noexcept with gil: if __ensure_handle_data(stream, "UVStream alloc buffer callback") == 0: @@ -945,9 +960,11 @@ cdef void __uv_stream_buffered_alloc(uv.uv_handle_t* stream, uvbuf.len = pybuf.len -cdef void __uv_stream_buffered_on_read(uv.uv_stream_t* stream, - ssize_t nread, - const uv.uv_buf_t* buf) with gil: +cdef void __uv_stream_buffered_on_read( + uv.uv_stream_t* stream, + ssize_t nread, + const uv.uv_buf_t* buf, +) noexcept with gil: if __ensure_handle_data(stream, "UVStream buffered read callback") == 0: diff --git a/uvloop/handles/streamserver.pyx b/uvloop/handles/streamserver.pyx index 6e0f3576..99933177 100644 --- a/uvloop/handles/streamserver.pyx +++ b/uvloop/handles/streamserver.pyx @@ -121,8 +121,10 @@ cdef class UVStreamServer(UVSocketHandle): transport._force_close(exc) -cdef void __uv_streamserver_on_listen(uv.uv_stream_t* handle, - int status) with gil: +cdef void __uv_streamserver_on_listen( + uv.uv_stream_t* handle, + int status, +) noexcept with gil: # callback for uv_listen diff --git a/uvloop/handles/tcp.pyx b/uvloop/handles/tcp.pyx index 8c65f69e..d5fe827a 100644 --- a/uvloop/handles/tcp.pyx +++ b/uvloop/handles/tcp.pyx @@ -204,7 +204,10 @@ cdef class _TCPConnectRequest(UVRequest): raise exc -cdef void __tcp_connect_callback(uv.uv_connect_t* req, int status) with gil: +cdef void __tcp_connect_callback( + uv.uv_connect_t* req, + int status, +) noexcept with gil: cdef: _TCPConnectRequest wrapper TCPTransport transport diff --git a/uvloop/handles/timer.pyx b/uvloop/handles/timer.pyx index 6ea5ff50..86d46ef0 100644 --- a/uvloop/handles/timer.pyx +++ b/uvloop/handles/timer.pyx @@ -72,7 +72,9 @@ cdef class UVTimer(UVHandle): return handle -cdef void __uvtimer_callback(uv.uv_timer_t* handle) with gil: +cdef void __uvtimer_callback( + uv.uv_timer_t* handle, +) noexcept with gil: if __ensure_handle_data(handle, "UVTimer callback") == 0: return diff --git a/uvloop/handles/udp.pyx b/uvloop/handles/udp.pyx index b92fdfd7..bbe60d56 100644 --- a/uvloop/handles/udp.pyx +++ b/uvloop/handles/udp.pyx @@ -305,11 +305,13 @@ cdef class UDPTransport(UVBaseTransport): self._send(data, addr) -cdef void __uv_udp_on_receive(uv.uv_udp_t* handle, - ssize_t nread, - const uv.uv_buf_t* buf, - const system.sockaddr* addr, - unsigned flags) with gil: +cdef void __uv_udp_on_receive( + uv.uv_udp_t* handle, + ssize_t nread, + const uv.uv_buf_t* buf, + const system.sockaddr* addr, + unsigned flags +) noexcept with gil: if __ensure_handle_data(handle, "UDPTransport receive callback") == 0: @@ -348,9 +350,9 @@ cdef void __uv_udp_on_receive(uv.uv_udp_t* handle, pyaddr = None elif addr.sa_family == uv.AF_UNSPEC: # https://github.com/MagicStack/uvloop/issues/304 - IF UNAME_SYSNAME == "Linux": + if system.PLATFORM_IS_LINUX: pyaddr = None - ELSE: + else: pyaddr = '' else: try: @@ -375,7 +377,10 @@ cdef void __uv_udp_on_receive(uv.uv_udp_t* handle, udp._error(exc, False) -cdef void __uv_udp_on_send(uv.uv_udp_send_t* req, int status) with gil: +cdef void __uv_udp_on_send( + uv.uv_udp_send_t* req, + int status, +) noexcept with gil: if req.data is NULL: # Shouldn't happen as: diff --git a/uvloop/loop.pyi b/uvloop/loop.pyi index 05d7714b..9c8c4623 100644 --- a/uvloop/loop.pyi +++ b/uvloop/loop.pyi @@ -271,7 +271,10 @@ class Loop: sock: Optional[socket] = ..., ) -> tuple[asyncio.BaseProtocol, _ProtocolT]: ... async def shutdown_asyncgens(self) -> None: ... - async def shutdown_default_executor(self) -> None: ... + async def shutdown_default_executor( + self, + timeout: Optional[float] = ..., + ) -> None: ... # Loop doesn't implement these, but since they are marked as abstract in typeshed, # we have to put them in so mypy thinks the base methods are overridden async def sendfile( diff --git a/uvloop/loop.pyx b/uvloop/loop.pyx index f6cb4fca..a767c130 100644 --- a/uvloop/loop.pyx +++ b/uvloop/loop.pyx @@ -38,7 +38,7 @@ from cpython cimport ( PyBytes_AsStringAndSize, Py_SIZE, PyBytes_AS_STRING, PyBUF_WRITABLE ) -from cpython.pycapsule cimport PyCapsule_New +from cpython.pycapsule cimport PyCapsule_New, PyCapsule_GetPointer from . import _noop @@ -3200,7 +3200,7 @@ cdef class Loop: }) @cython.iterable_coroutine - async def shutdown_default_executor(self): + async def shutdown_default_executor(self, timeout=None): """Schedule the shutdown of the default executor.""" self._executor_shutdown_called = True if self._default_executor is None: @@ -3211,7 +3211,16 @@ cdef class Loop: try: await future finally: - thread.join() + thread.join(timeout) + + if thread.is_alive(): + warnings.warn( + "The executor did not finishing joining " + f"its threads within {timeout} seconds.", + RuntimeWarning, + stacklevel=2 + ) + self._default_executor.shutdown(wait=False) def _do_shutdown(self, future): try: @@ -3230,9 +3239,15 @@ def libuv_get_version(): return uv.uv_version() -cdef void __loop_alloc_buffer(uv.uv_handle_t* uvhandle, - size_t suggested_size, - uv.uv_buf_t* buf) with gil: +def _testhelper_unwrap_capsuled_pointer(obj): + return PyCapsule_GetPointer(obj, NULL) + + +cdef void __loop_alloc_buffer( + uv.uv_handle_t* uvhandle, + size_t suggested_size, + uv.uv_buf_t* buf +) noexcept with gil: cdef: Loop loop = (uvhandle.data)._loop @@ -3330,7 +3345,7 @@ cdef vint __forking = 0 cdef Loop __forking_loop = None -cdef void __get_fork_handler() nogil: +cdef void __get_fork_handler() noexcept nogil: with gil: if (__forking and __forking_loop is not None and __forking_loop.active_process_handler is not None):