diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 29e7a7a4..d49bb4e7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -83,6 +83,7 @@ jobs: - "cp311-*" - "cp312-*" - "cp313-*" + - "cp314-*" cibw_arch: ["x86_64", "aarch64", "universal2"] exclude: - os: ubuntu-latest diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b5bdc6f3..bdf27875 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,6 +22,7 @@ jobs: - "3.11" - "3.12" - "3.13" + - "3.14" os: [ubuntu-latest, macos-latest] env: diff --git a/pyproject.toml b/pyproject.toml index a656b4f8..d7ee1f1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "uvloop" description = "Fast implementation of asyncio event loop on top of libuv" authors = [{name = "Yury Selivanov", email = "yury@magic.io"}] -requires-python = '>=3.8.0' +requires-python = '>=3.8.1' readme = "README.rst" license = {text = "MIT License"} dynamic = ["version"] @@ -38,9 +38,9 @@ test = [ # their combination breaks too often # (example breakage: https://gitlab.com/pycqa/flake8/issues/427) 'aiohttp>=3.10.5', - 'flake8~=5.0', + 'flake8~=6.1', 'psutil', - 'pycodestyle~=2.9.0', + 'pycodestyle~=2.11.0', 'pyOpenSSL~=23.0.0', 'mypy>=0.800', ] diff --git a/uvloop/__init__.py b/uvloop/__init__.py index 9bb6592b..638b874c 100644 --- a/uvloop/__init__.py +++ b/uvloop/__init__.py @@ -3,20 +3,19 @@ import sys as _sys import warnings as _warnings -from asyncio.events import BaseDefaultEventLoopPolicy as __BasePolicy - from . import includes as __includes # NOQA from .loop import Loop as __BaseLoop # NOQA from ._version import __version__ # NOQA -__all__ = ('new_event_loop', 'install', 'EventLoopPolicy') +__all__: _typing.Tuple[str, ...] = ('new_event_loop', 'run') +_AbstractEventLoop = __asyncio.AbstractEventLoop _T = _typing.TypeVar("_T") -class Loop(__BaseLoop, __asyncio.AbstractEventLoop): # type: ignore[misc] +class Loop(__BaseLoop, _AbstractEventLoop): # type: ignore[misc] pass @@ -25,18 +24,6 @@ def new_event_loop() -> Loop: return 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()) - - if _typing.TYPE_CHECKING: def run( main: _typing.Coroutine[_typing.Any, _typing.Any, _T], @@ -114,7 +101,7 @@ async def wrapper(): ) -def _cancel_all_tasks(loop: __asyncio.AbstractEventLoop) -> None: +def _cancel_all_tasks(loop: _AbstractEventLoop) -> None: # Copied from python/cpython to_cancel = __asyncio.all_tasks(loop) @@ -139,30 +126,108 @@ def _cancel_all_tasks(loop: __asyncio.AbstractEventLoop) -> None: }) -class EventLoopPolicy(__BasePolicy): - """Event loop policy. +_deprecated_names = ('install', 'EventLoopPolicy') + + +if _sys.version_info[:2] < (3, 16): + __all__ += _deprecated_names + + +def __getattr__(name: str) -> _typing.Any: + if name not in _deprecated_names: + raise AttributeError(f"module 'uvloop' has no attribute '{name}'") + elif _sys.version_info[:2] >= (3, 16): + raise AttributeError( + f"module 'uvloop' has no attribute '{name}' " + f"(it was removed in Python 3.16, use uvloop.run() instead)" + ) + + import threading + + def install() -> None: + """A helper function to install uvloop policy. + + This function is deprecated and will be removed in Python 3.16. + Use `uvloop.run()` instead. + """ + 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()) + + class EventLoopPolicy( + # This is to avoid a mypy error about AbstractEventLoopPolicy + getattr(__asyncio, 'AbstractEventLoopPolicy') # type: ignore[misc] + ): + """Event loop policy for uvloop. + + This class is deprecated and will be removed in Python 3.16. + Use `uvloop.run()` instead. + + >>> import asyncio + >>> import uvloop + >>> asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + >>> asyncio.get_event_loop() + + """ + + def _loop_factory(self) -> Loop: + return new_event_loop() + + if _typing.TYPE_CHECKING: + # EventLoopPolicy 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. This is the same approach + # taken for the Windows event loop policy classes in typeshed. + def get_child_watcher(self) -> _typing.NoReturn: + ... + + def set_child_watcher( + self, watcher: _typing.Any + ) -> _typing.NoReturn: + ... + + class _Local(threading.local): + _loop: _typing.Optional[_AbstractEventLoop] = None + + def __init__(self) -> None: + self._local = self._Local() + + def get_event_loop(self) -> _AbstractEventLoop: + """Get the event loop for the current context. + + Returns an instance of EventLoop or raises an exception. + """ + if self._local._loop is None: + raise RuntimeError( + 'There is no current event loop in thread %r.' + % threading.current_thread().name + ) - The preferred way to make your application use uvloop: + return self._local._loop - >>> import asyncio - >>> import uvloop - >>> asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) - >>> asyncio.get_event_loop() - - """ + def set_event_loop( + self, loop: _typing.Optional[_AbstractEventLoop] + ) -> None: + """Set the event loop.""" + if loop is not None and not isinstance(loop, _AbstractEventLoop): + raise TypeError( + f"loop must be an instance of AbstractEventLoop or None, " + f"not '{type(loop).__name__}'" + ) + self._local._loop = loop - def _loop_factory(self) -> Loop: - return new_event_loop() + def new_event_loop(self) -> Loop: + """Create a new event loop. - if _typing.TYPE_CHECKING: - # EventLoopPolicy 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. This is the same approach taken - # for the Windows event loop policy classes in typeshed. - def get_child_watcher(self) -> _typing.NoReturn: - ... + You must call set_event_loop() to make this the current event loop. + """ + return self._loop_factory() - def set_child_watcher( - self, watcher: _typing.Any - ) -> _typing.NoReturn: - ... + globals()['install'] = install + globals()['EventLoopPolicy'] = EventLoopPolicy + return globals()[name] diff --git a/uvloop/includes/stdlib.pxi b/uvloop/includes/stdlib.pxi index 4152b8a7..02605a08 100644 --- a/uvloop/includes/stdlib.pxi +++ b/uvloop/includes/stdlib.pxi @@ -44,7 +44,7 @@ cdef aio_isfuture = getattr(asyncio, 'isfuture', None) cdef aio_get_running_loop = getattr(asyncio, '_get_running_loop', None) cdef aio_set_running_loop = getattr(asyncio, '_set_running_loop', None) cdef aio_debug_wrapper = getattr(asyncio.coroutines, 'debug_wrapper', None) -cdef aio_AbstractChildWatcher = asyncio.AbstractChildWatcher +cdef aio_AbstractChildWatcher = getattr(asyncio, "AbstractChildWatcher", ()) cdef aio_Transport = asyncio.Transport cdef aio_FlowControlMixin = asyncio.transports._FlowControlMixin