commit python-anyio for openSUSE:Factory
Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-anyio for openSUSE:Factory checked in at 2021-11-29 17:28:40 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-anyio (Old) and /work/SRC/openSUSE:Factory/.python-anyio.new.31177 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Package is "python-anyio" Mon Nov 29 17:28:40 2021 rev:7 rq:934534 version:3.4.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-anyio/python-anyio.changes 2021-10-27 22:21:12.727198859 +0200 +++ /work/SRC/openSUSE:Factory/.python-anyio.new.31177/python-anyio.changes 2021-12-02 02:22:04.193274130 +0100 @@ -1,0 +2,14 @@ +Mon Nov 29 12:01:51 UTC 2021 - Dirk M��ller <dmueller@suse.com> + +- update to 3.4.0: + * Added context propagation to/from worker threads in ``to_thread.run_sync()``, + ``from_thread.run()`` and ``from_thread.run_sync()`` + * Fixed race condition in ``Lock`` and ``Semaphore`` classes when a task waiting on ``acquire()`` + is cancelled while another task is waiting to acquire the same primitive + * Fixed async context manager's ``__aexit__()`` method not being called in + ``BlockingPortal.wrap_async_context_manager()`` if the host task is cancelled + * Fixed worker threads being marked as being event loop threads in sniffio + * Fixed task parent ID not getting set to the correct value on asyncio + * Enabled the test suite to run without IPv6 support, trio or pytest plugin autoloading + +------------------------------------------------------------------- Old: ---- anyio-3.3.4.tar.gz New: ---- anyio-3.4.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-anyio.spec ++++++ --- /var/tmp/diff_new_pack.NqEphl/_old 2021-12-02 02:22:04.661272497 +0100 +++ /var/tmp/diff_new_pack.NqEphl/_new 2021-12-02 02:22:04.665272483 +0100 @@ -19,12 +19,13 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %define skip_python2 1 Name: python-anyio -Version: 3.3.4 +Version: 3.4.0 Release: 0 Summary: High level compatibility layer for asynchronous event loop implementations License: MIT URL: https://github.com/agronholm/anyio Source: https://files.pythonhosted.org/packages/source/a/anyio/anyio-%{version}.tar.gz +BuildRequires: %{python_module contextlib2 if %python-base < 3.7} BuildRequires: %{python_module dataclasses if %python-base < 3.7} BuildRequires: %{python_module idna >= 2.8} BuildRequires: %{python_module setuptools_scm} @@ -49,6 +50,7 @@ Requires: python-typing_extensions %endif %if 0%{?python_version_nodots} < 37 +Requires: python-contextvars Requires: python-dataclasses %endif Suggests: python-trio >= 0.16 ++++++ anyio-3.3.4.tar.gz -> anyio-3.4.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/.github/workflows/codeqa-test.yml new/anyio-3.4.0/.github/workflows/codeqa-test.yml --- old/anyio-3.3.4/.github/workflows/codeqa-test.yml 2021-10-16 12:45:31.000000000 +0200 +++ new/anyio-3.4.0/.github/workflows/codeqa-test.yml 2021-11-23 00:57:30.000000000 +0100 @@ -66,6 +66,8 @@ run: pip install .[test,trio] coveralls - name: Test with pytest run: coverage run -m pytest + env: + PYTEST_DISABLE_PLUGIN_AUTOLOAD: 1 - name: Upload Coverage run: coveralls --service=github env: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/PKG-INFO new/anyio-3.4.0/PKG-INFO --- old/anyio-3.3.4/PKG-INFO 2021-10-16 12:45:40.520680400 +0200 +++ new/anyio-3.4.0/PKG-INFO 2021-11-23 00:57:44.761705900 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: anyio -Version: 3.3.4 +Version: 3.4.0 Summary: High level compatibility layer for multiple asynchronous event loop implementations Home-page: UNKNOWN Author: Alex Gr��nholm diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/docs/subprocesses.rst new/anyio-3.4.0/docs/subprocesses.rst --- old/anyio-3.3.4/docs/subprocesses.rst 2021-10-16 12:45:31.000000000 +0200 +++ new/anyio-3.4.0/docs/subprocesses.rst 2021-11-23 00:57:30.000000000 +0100 @@ -108,7 +108,7 @@ * If a cancellable call is cancelled during execution on the worker process, the worker process will be killed * The worker process imports the parent's ``__main__`` module, so guarding for any import time side - effects using ``if __name__ == '__main__':`` is required to avoid inifinite recursion + effects using ``if __name__ == '__main__':`` is required to avoid infinite recursion * ``sys.stdin`` and ``sys.stdout``, ``sys.stderr`` are redirected to ``/dev/null`` so :func:`print` and :func:`input` won't work * Worker processes terminate after 5 minutes of inactivity, or when the event loop is finished diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/docs/tasks.rst new/anyio-3.4.0/docs/tasks.rst --- old/anyio-3.3.4/docs/tasks.rst 2021-10-16 12:45:31.000000000 +0200 +++ new/anyio-3.4.0/docs/tasks.rst 2021-11-23 00:57:30.000000000 +0100 @@ -90,3 +90,17 @@ exception, :exc:`~ExceptionGroup` is raised which contains both exception objects. Unfortunately this complicates any code that wishes to catch a specific exception because it could be wrapped in an :exc:`~ExceptionGroup`. + +Context propagation +------------------- + +Whenever a new task is spawned, `context`_ will be copied to the new task. It is important to note +*which* content will be copied to the newly spawned task. It is not the context of the task group's +host task that will be copied, but the context of the task that calls +:meth:`TaskGroup.start() <.abc.TaskGroup.start>` or +:meth:`TaskGroup.start_soon() <.abc.TaskGroup.start_soon>`. + +.. note:: Context propagation **does not work** on asyncio when using Python 3.6, as asyncio + support for this only landed in v3.7. + +.. _context: https://docs.python.org/3/library/contextvars.html diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/docs/threads.rst new/anyio-3.4.0/docs/threads.rst --- old/anyio-3.3.4/docs/threads.rst 2021-10-16 12:45:31.000000000 +0200 +++ new/anyio-3.4.0/docs/threads.rst 2021-11-23 00:57:30.000000000 +0100 @@ -177,3 +177,15 @@ .. note:: You cannot use wrapped async context managers in synchronous callbacks inside the event loop thread. + +Context propagation +------------------- + +When running functions in worker threads, the current context is copied to the worker thread. +Therefore any context variables available on the task will also be available to the code running +on the thread. As always with context variables, any changes made to them will not propagate back +to the calling asynchronous task. + +When calling asynchronous code from worker threads, context is again copied to the task that calls +the target function in the event loop thread. Note, however, that this **does not work** on asyncio +when running on Python 3.6. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/docs/versionhistory.rst new/anyio-3.4.0/docs/versionhistory.rst --- old/anyio-3.3.4/docs/versionhistory.rst 2021-10-16 12:45:31.000000000 +0200 +++ new/anyio-3.4.0/docs/versionhistory.rst 2021-11-23 00:57:30.000000000 +0100 @@ -3,6 +3,24 @@ This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_. +**3.4.0** + +- Added context propagation to/from worker threads in ``to_thread.run_sync()``, + ``from_thread.run()`` and ``from_thread.run_sync()`` + (`#363 <https://github.com/agronholm/anyio/issues/363>`_; partially based on a PR by Sebasti��n + Ram��rez) + + **NOTE**: Requires Python 3.7 to work properly on asyncio! +- Fixed race condition in ``Lock`` and ``Semaphore`` classes when a task waiting on ``acquire()`` + is cancelled while another task is waiting to acquire the same primitive + (`#387 <https://github.com/agronholm/anyio/issues/387>`_) +- Fixed async context manager's ``__aexit__()`` method not being called in + ``BlockingPortal.wrap_async_context_manager()`` if the host task is cancelled + (`#381 <https://github.com/agronholm/anyio/issues/381>`_; PR by Jonathan Slenders) +- Fixed worker threads being marked as being event loop threads in sniffio +- Fixed task parent ID not getting set to the correct value on asyncio +- Enabled the test suite to run without IPv6 support, trio or pytest plugin autoloading + **3.3.4** - Fixed ``BrokenResourceError`` instead of ``EndOfStream`` being raised in ``TLSStream`` when the diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/pyproject.toml new/anyio-3.4.0/pyproject.toml --- old/anyio-3.3.4/pyproject.toml 2021-10-16 12:45:31.000000000 +0200 +++ new/anyio-3.4.0/pyproject.toml 2021-11-23 00:57:30.000000000 +0100 @@ -32,7 +32,7 @@ disallow_subclassing_any = false [tool.pytest.ini_options] -addopts = "-rsx --tb=short --strict-config --strict-markers" +addopts = "-rsx --tb=short --strict-config --strict-markers -p anyio -p no:asyncio" testpaths = ["tests"] # Ignore resource warnings due to a CPython/Windows bug (https://bugs.python.org/issue44428) filterwarnings = [ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/setup.cfg new/anyio-3.4.0/setup.cfg --- old/anyio-3.3.4/setup.cfg 2021-10-16 12:45:40.520680400 +0200 +++ new/anyio-3.4.0/setup.cfg 2021-11-23 00:57:44.761705900 +0100 @@ -28,6 +28,7 @@ python_requires = >= 3.6.2 zip_safe = False install_requires = + contextvars; python_version < '3.7' dataclasses; python_version < '3.7' idna >= 2.8 sniffio >= 1.1 @@ -42,6 +43,7 @@ [options.extras_require] test = mock >= 4; python_version < '3.8' + contextlib2; python_version < '3.7' coverage[toml] >= 4.5 hypothesis >= 4.0 pytest >= 6.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/src/anyio/_backends/_asyncio.py new/anyio-3.4.0/src/anyio/_backends/_asyncio.py --- old/anyio-3.3.4/src/anyio/_backends/_asyncio.py 2021-10-16 12:45:31.000000000 +0200 +++ new/anyio-3.4.0/src/anyio/_backends/_asyncio.py 2021-11-23 00:57:30.000000000 +0100 @@ -7,6 +7,7 @@ from asyncio.base_events import _run_until_complete_cb # type: ignore from collections import OrderedDict, deque from concurrent.futures import Future +from contextvars import Context, copy_context from dataclasses import dataclass from functools import partial, wraps from inspect import ( @@ -22,6 +23,8 @@ Mapping, Optional, Sequence, Set, Tuple, Type, TypeVar, Union, cast) from weakref import WeakKeyDictionary +import sniffio + from .. import CapacityLimiterStatistics, EventStatistics, TaskInfo, abc from .._core._compat import DeprecatedAsyncContextManager, DeprecatedAwaitable from .._core._eventloop import claim_worker_thread, threadlocals @@ -515,8 +518,9 @@ class _AsyncioTaskStatus(abc.TaskStatus): - def __init__(self, future: asyncio.Future): + def __init__(self, future: asyncio.Future, parent_id: int): self._future = future + self._parent_id = parent_id def started(self, value: object = None) -> None: try: @@ -524,6 +528,9 @@ except asyncio.InvalidStateError: raise RuntimeError("called 'started' twice on the same task status") from None + task = cast(asyncio.Task, current_task()) + _task_states[task].parent_id = self._parent_id + class TaskGroup(abc.TaskGroup): def __init__(self) -> None: @@ -653,7 +660,11 @@ kwargs = {} if task_status_future: - kwargs['task_status'] = _AsyncioTaskStatus(task_status_future) + parent_id = id(current_task()) + kwargs['task_status'] = _AsyncioTaskStatus(task_status_future, + id(self.cancel_scope._host_task)) + else: + parent_id = id(self.cancel_scope._host_task) coro = func(*args, **kwargs) if not asyncio.iscoroutine(coro): @@ -668,7 +679,7 @@ task.add_done_callback(task_done) # Make the spawned task inherit the task group's cancel scope - _task_states[task] = TaskState(parent_id=id(current_task()), name=name, + _task_states[task] = TaskState(parent_id=parent_id, name=name, cancel_scope=self.cancel_scope) self.cancel_scope._tasks.add(task) return task @@ -710,7 +721,7 @@ self.workers = workers self.idle_workers = idle_workers self.loop = root_task._loop - self.queue: Queue[Union[Tuple[Callable, tuple, asyncio.Future], None]] = Queue(2) + self.queue: Queue[Union[Tuple[Context, Callable, tuple, asyncio.Future], None]] = Queue(2) self.idle_since = current_time() self.stopping = False @@ -735,12 +746,12 @@ # Shutdown command received return - func, args, future = item + context, func, args, future = item if not future.cancelled(): result = None exception: Optional[BaseException] = None try: - result = func(*args) + result = context.run(func, *args) except BaseException as exc: exception = exc @@ -801,7 +812,9 @@ expired_worker.root_task.remove_done_callback(expired_worker.stop) expired_worker.stop() - worker.queue.put_nowait((func, args, future)) + context = copy_context() + context.run(sniffio.current_async_library_cvar.set, None) + worker.queue.put_nowait((context, func, args, future)) return await future @@ -818,7 +831,11 @@ f: concurrent.futures.Future[T_Retval] = Future() loop = loop or threadlocals.loop - loop.call_soon_threadsafe(wrapper) + if sys.version_info < (3, 7): + loop.call_soon_threadsafe(copy_context().run, wrapper) + else: + loop.call_soon_threadsafe(wrapper) + return f.result() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/src/anyio/_backends/_trio.py new/anyio-3.4.0/src/anyio/_backends/_trio.py --- old/anyio-3.3.4/src/anyio/_backends/_trio.py 2021-10-16 12:45:31.000000000 +0200 +++ new/anyio-3.4.0/src/anyio/_backends/_trio.py 2021-11-23 00:57:30.000000000 +0100 @@ -2,6 +2,7 @@ import math import socket from concurrent.futures import Future +from contextvars import copy_context from dataclasses import dataclass from functools import partial from io import IOBase @@ -11,6 +12,7 @@ Any, Awaitable, Callable, Collection, ContextManager, Coroutine, Deque, Dict, Generic, List, Mapping, NoReturn, Optional, Sequence, Set, Tuple, Type, TypeVar, Union) +import sniffio import trio.from_thread from outcome import Error, Outcome, Value from trio.socket import SocketType as TrioSocketType @@ -37,6 +39,10 @@ else: from trio.lowlevel import wait_readable, wait_writable +try: + from trio.lowlevel import open_process as trio_open_process +except ImportError: + from trio import open_process as trio_open_process T_Retval = TypeVar('T_Retval') T_SockAddr = TypeVar('T_SockAddr', str, IPSockAddrType) @@ -167,10 +173,36 @@ with claim_worker_thread('trio'): return func(*args) - return await run_sync(wrapper, cancellable=cancellable, limiter=limiter) + # TODO: remove explicit context copying when trio 0.20 is the minimum requirement + context = copy_context() + context.run(sniffio.current_async_library_cvar.set, None) + return await run_sync(context.run, wrapper, cancellable=cancellable, limiter=limiter) + + +# TODO: remove this workaround when trio 0.20 is the minimum requirement +def run_async_from_thread(fn: Callable[..., Awaitable[T_Retval]], *args: Any) -> T_Retval: + async def wrapper() -> Optional[T_Retval]: + retval: T_Retval + + async def inner() -> None: + nonlocal retval + __tracebackhide__ = True + retval = await fn(*args) + + async with trio.open_nursery() as n: + context.run(n.start_soon, inner) + + __tracebackhide__ = True + return retval + + context = copy_context() + context.run(sniffio.current_async_library_cvar.set, 'trio') + return trio.from_thread.run(wrapper) + -run_async_from_thread = trio.from_thread.run -run_sync_from_thread = trio.from_thread.run_sync +def run_sync_from_thread(fn: Callable[..., T_Retval], *args: Any) -> T_Retval: + # TODO: remove explicit context copying when trio 0.20 is the minimum requirement + return trio.from_thread.run_sync(copy_context().run, fn, *args) class BlockingPortal(abc.BlockingPortal): @@ -183,9 +215,11 @@ def _spawn_task_from_thread(self, func: Callable, args: tuple, kwargs: Dict[str, Any], name: object, future: Future) -> None: + context = copy_context() + context.run(sniffio.current_async_library_cvar.set, 'trio') return trio.from_thread.run_sync( - partial(self._task_group.start_soon, name=name), self._call_func, func, args, kwargs, - future, trio_token=self._token) + context.run, partial(self._task_group.start_soon, name=name), self._call_func, func, + args, kwargs, future, trio_token=self._token) # @@ -283,7 +317,7 @@ stdin: int, stdout: int, stderr: int, cwd: Union[str, bytes, PathLike, None] = None, env: Optional[Mapping[str, str]] = None) -> Process: - process = await trio.open_process(command, stdin=stdin, stdout=stdout, stderr=stderr, + process = await trio_open_process(command, stdin=stdin, stdout=stdout, stderr=stderr, shell=shell, cwd=cwd, env=env) stdin_stream = SendStreamWrapper(process.stdin) if process.stdin else None stdout_stream = ReceiveStreamWrapper(process.stdout) if process.stdout else None diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/src/anyio/_core/_eventloop.py new/anyio-3.4.0/src/anyio/_core/_eventloop.py --- old/anyio-3.3.4/src/anyio/_core/_eventloop.py 2021-10-16 12:45:31.000000000 +0200 +++ new/anyio-3.4.0/src/anyio/_core/_eventloop.py 2021-11-23 00:57:30.000000000 +0100 @@ -123,11 +123,9 @@ def claim_worker_thread(backend: str) -> Generator[Any, None, None]: module = sys.modules['anyio._backends._' + backend] threadlocals.current_async_module = module - token = sniffio.current_async_library_cvar.set(backend) try: yield finally: - sniffio.current_async_library_cvar.reset(token) del threadlocals.current_async_module diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/src/anyio/_core/_exceptions.py new/anyio-3.4.0/src/anyio/_core/_exceptions.py --- old/anyio-3.3.4/src/anyio/_core/_exceptions.py 2021-10-16 12:45:31.000000000 +0200 +++ new/anyio-3.4.0/src/anyio/_core/_exceptions.py 2021-11-23 00:57:30.000000000 +0100 @@ -4,7 +4,7 @@ class BrokenResourceError(Exception): """ - Raised when trying to use a resource that has been rendered unusuable due to external causes + Raised when trying to use a resource that has been rendered unusable due to external causes (e.g. a send stream whose peer has disconnected). """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/src/anyio/_core/_synchronization.py new/anyio-3.4.0/src/anyio/_core/_synchronization.py --- old/anyio-3.3.4/src/anyio/_core/_synchronization.py 2021-10-16 12:45:31.000000000 +0200 +++ new/anyio-3.4.0/src/anyio/_core/_synchronization.py 2021-11-23 00:57:30.000000000 +0100 @@ -127,6 +127,8 @@ except BaseException: if not event.is_set(): self._waiters.remove(token) + elif self._owner_task == task: + self.release() raise @@ -302,6 +304,8 @@ except BaseException: if not event.is_set(): self._waiters.remove(event) + else: + self.release() raise else: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/src/anyio/from_thread.py new/anyio-3.4.0/src/anyio/from_thread.py --- old/anyio-3.3.4/src/anyio/from_thread.py 2021-10-16 12:45:31.000000000 +0200 +++ new/anyio-3.4.0/src/anyio/from_thread.py 2021-11-23 00:57:30.000000000 +0100 @@ -70,7 +70,7 @@ _exit_future: Future _exit_event: Event _exit_exc_info: Tuple[Optional[Type[BaseException]], Optional[BaseException], - Optional[TracebackType]] + Optional[TracebackType]] = (None, None, None) def __init__(self, async_cm: AsyncContextManager[T_co], portal: 'BlockingPortal'): self._async_cm = async_cm @@ -86,8 +86,18 @@ else: self._enter_future.set_result(value) - await self._exit_event.wait() - return await self._async_cm.__aexit__(*self._exit_exc_info) + try: + # Wait for the sync context manager to exit. + # This next statement can raise `get_cancelled_exc_class()` if + # something went wrong in a task group in this async context + # manager. + await self._exit_event.wait() + finally: + # In case of cancellation, it could be that we end up here before + # `_BlockingAsyncContextManager.__exit__` is called, and an + # `_exit_exc_info` has been set. + result = await self._async_cm.__aexit__(*self._exit_exc_info) + return result def __enter__(self) -> T_co: self._enter_future = Future() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/src/anyio.egg-info/PKG-INFO new/anyio-3.4.0/src/anyio.egg-info/PKG-INFO --- old/anyio-3.3.4/src/anyio.egg-info/PKG-INFO 2021-10-16 12:45:40.000000000 +0200 +++ new/anyio-3.4.0/src/anyio.egg-info/PKG-INFO 2021-11-23 00:57:44.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: anyio -Version: 3.3.4 +Version: 3.4.0 Summary: High level compatibility layer for multiple asynchronous event loop implementations Home-page: UNKNOWN Author: Alex Gr��nholm diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/src/anyio.egg-info/requires.txt new/anyio-3.4.0/src/anyio.egg-info/requires.txt --- old/anyio-3.3.4/src/anyio.egg-info/requires.txt 2021-10-16 12:45:40.000000000 +0200 +++ new/anyio-3.4.0/src/anyio.egg-info/requires.txt 2021-11-23 00:57:44.000000000 +0100 @@ -2,6 +2,7 @@ sniffio>=1.1 [:python_version < "3.7"] +contextvars dataclasses [:python_version < "3.8"] @@ -18,6 +19,9 @@ pytest-mock>=3.6.1 trustme +[test:python_version < "3.7"] +contextlib2 + [test:python_version < "3.7" and (platform_python_implementation == "CPython" and platform_system != "Windows")] uvloop<0.15 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/tests/conftest.py new/anyio-3.4.0/tests/conftest.py --- old/anyio-3.3.4/tests/conftest.py 2021-10-16 12:45:31.000000000 +0200 +++ new/anyio-3.4.0/tests/conftest.py 2021-11-23 00:57:30.000000000 +0100 @@ -22,7 +22,7 @@ else: uvloop_policy = uvloop.EventLoopPolicy() -pytest_plugins = ['pytester'] +pytest_plugins = ['pytester', 'pytest_mock'] @pytest.fixture(params=[ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/tests/test_from_thread.py new/anyio-3.4.0/tests/test_from_thread.py --- old/anyio-3.3.4/tests/test_from_thread.py 2021-10-16 12:45:31.000000000 +0200 +++ new/anyio-3.4.0/tests/test_from_thread.py 2021-11-23 00:57:30.000000000 +0100 @@ -3,22 +3,29 @@ import time from concurrent.futures import CancelledError from contextlib import suppress -from typing import Any, Dict, List, NoReturn, Optional +from contextvars import ContextVar +from typing import Any, AsyncGenerator, Dict, List, NoReturn, Optional import pytest from _pytest.logging import LogCaptureFixture from anyio import ( - Event, from_thread, get_cancelled_exc_class, get_current_task, run, sleep, to_thread, - wait_all_tasks_blocked) + Event, create_task_group, from_thread, get_cancelled_exc_class, get_current_task, run, sleep, + to_thread, wait_all_tasks_blocked) from anyio.abc import TaskStatus from anyio.from_thread import BlockingPortal, start_blocking_portal +from anyio.lowlevel import checkpoint if sys.version_info >= (3, 8): from typing import Literal else: from typing_extensions import Literal +if sys.version_info >= (3, 7): + from contextlib import asynccontextmanager +else: + from contextlib2 import asynccontextmanager + pytestmark = pytest.mark.anyio @@ -117,6 +124,22 @@ exc = pytest.raises(RuntimeError, from_thread.run, foo) exc.match('This function can only be run from an AnyIO worker thread') + async def test_contextvar_propagation(self, anyio_backend_name: str) -> None: + if anyio_backend_name == 'asyncio' and sys.version_info < (3, 7): + pytest.skip('Asyncio does not propagate context before Python 3.7') + + var = ContextVar('var', default=1) + + async def async_func() -> int: + await checkpoint() + return var.get() + + def worker() -> int: + var.set(6) + return from_thread.run(async_func) + + assert await to_thread.run_sync(worker) == 6 + class TestRunSyncFromThread: def test_run_sync_from_unclaimed_thread(self) -> None: @@ -126,6 +149,15 @@ exc = pytest.raises(RuntimeError, from_thread.run_sync, foo) exc.match('This function can only be run from an AnyIO worker thread') + async def test_contextvar_propagation(self) -> None: + var = ContextVar('var', default=1) + + def worker() -> int: + var.set(6) + return from_thread.run_sync(var.get) + + assert await to_thread.run_sync(worker) == 6 + class TestBlockingPortal: class AsyncCM: @@ -318,6 +350,23 @@ assert cm == 'test' raise Exception('should be ignored') + def test_async_context_manager_exception_in_task_group( + self, anyio_backend_name: str, anyio_backend_options: Dict[str, Any]) -> None: + """Regression test for #381.""" + async def failing_func() -> None: + 0 / 0 + + @asynccontextmanager + async def run_in_context() -> AsyncGenerator[None, None]: + async with create_task_group() as tg: + tg.start_soon(failing_func) + yield + + with start_blocking_portal(anyio_backend_name, anyio_backend_options) as portal: + with pytest.raises(ZeroDivisionError): + with portal.wrap_async_context_manager(run_in_context()): + pass + def test_start_no_value(self, anyio_backend_name: str, anyio_backend_options: Dict[str, Any]) -> None: def taskfunc(*, task_status: TaskStatus) -> None: @@ -378,6 +427,35 @@ taskfunc, name='testname') # type: ignore[arg-type] assert start_value == 'testname' + def test_contextvar_propagation_sync(self, anyio_backend_name: str, + anyio_backend_options: Dict[str, Any]) -> None: + if anyio_backend_name == 'asyncio' and sys.version_info < (3, 7): + pytest.skip('Asyncio does not propagate context before Python 3.7') + + var = ContextVar('var', default=1) + var.set(6) + with start_blocking_portal(anyio_backend_name, anyio_backend_options) as portal: + propagated_value = portal.call(var.get) + + assert propagated_value == 6 + + def test_contextvar_propagation_async(self, anyio_backend_name: str, + anyio_backend_options: Dict[str, Any]) -> None: + if anyio_backend_name == 'asyncio' and sys.version_info < (3, 7): + pytest.skip('Asyncio does not propagate context before Python 3.7') + + var = ContextVar('var', default=1) + var.set(6) + + async def get_var() -> int: + await checkpoint() + return var.get() + + with start_blocking_portal(anyio_backend_name, anyio_backend_options) as portal: + propagated_value = portal.call(get_var) + + assert propagated_value == 6 + @pytest.mark.parametrize('anyio_backend', ['asyncio']) async def test_asyncio_run_sync_called(self, caplog: LogCaptureFixture) -> None: """Regression test for #357.""" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/tests/test_pytest_plugin.py new/anyio-3.4.0/tests/test_pytest_plugin.py --- old/anyio-3.3.4/tests/test_pytest_plugin.py 2021-10-16 12:45:31.000000000 +0200 +++ new/anyio-3.4.0/tests/test_pytest_plugin.py 2021-11-23 00:57:30.000000000 +0100 @@ -6,6 +6,8 @@ pytestmark = pytest.mark.filterwarnings( 'ignore:The TerminalReporter.writer attribute is deprecated:pytest.PytestDeprecationWarning:') +pytest_args = '-v', '-p', 'anyio', '-p', 'no:asyncio' + def test_plugin(testdir: Testdir) -> None: testdir.makeconftest( @@ -61,7 +63,7 @@ """ ) - result = testdir.runpytest('-v', '-p', 'no:asyncio') + result = testdir.runpytest(*pytest_args) result.assert_outcomes(passed=3 * len(get_all_backends()), skipped=len(get_all_backends())) @@ -134,7 +136,7 @@ """ ) - result = testdir.runpytest('-v', '-p', 'no:asyncio') + result = testdir.runpytest(*pytest_args) result.assert_outcomes(passed=2, failed=1, errors=2) @@ -171,7 +173,7 @@ """ ) - result = testdir.runpytest('-v', '-p', 'no:asyncio') + result = testdir.runpytest_subprocess(*pytest_args) result.assert_outcomes(passed=len(get_all_backends())) @@ -198,7 +200,7 @@ """ ) - result = testdir.runpytest('-v', '-p', 'no:asyncio') + result = testdir.runpytest_subprocess(*pytest_args) result.assert_outcomes(passed=len(get_all_backends())) @@ -229,7 +231,7 @@ """ ) - result = testdir.runpytest('-v', '-p', 'no:asyncio') + result = testdir.runpytest(*pytest_args) result.assert_outcomes(passed=len(get_all_backends()) + 1, xfailed=len(get_all_backends())) @@ -268,5 +270,5 @@ """ ) - result = testdir.runpytest('-v', '-p', 'no:asyncio') + result = testdir.runpytest(*pytest_args) result.assert_outcomes(passed=2 * len(get_all_backends()), xfailed=2 * len(get_all_backends())) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/tests/test_sockets.py new/anyio-3.4.0/tests/test_sockets.py --- old/anyio-3.3.4/tests/test_sockets.py 2021-10-16 12:45:31.000000000 +0200 +++ new/anyio-3.4.0/tests/test_sockets.py 2021-11-23 00:57:30.000000000 +0100 @@ -38,6 +38,20 @@ pytestmark = pytest.mark.anyio +# If a socket can bind to ::1, the current environment has IPv6 properly configured +has_ipv6 = False +if socket.has_ipv6: + s = socket.socket(AddressFamily.AF_INET6) + try: + s.bind(('::1', 0)) + except OSError: + pass + else: + has_ipv6 = True + finally: + s.close() + del s + @pytest.fixture def fake_localhost_dns(monkeypatch: MonkeyPatch) -> None: @@ -53,7 +67,7 @@ @pytest.fixture(params=[ pytest.param(AddressFamily.AF_INET, id='ipv4'), pytest.param(AddressFamily.AF_INET6, id='ipv6', - marks=[pytest.mark.skipif(not socket.has_ipv6, reason='no IPv6 support')]) + marks=[pytest.mark.skipif(not has_ipv6, reason='no IPv6 support')]) ]) def family(request: SubRequest) -> AnyIPAddressFamily: return request.param @@ -194,7 +208,7 @@ raw_socket = stream.extra(SocketAttribute.raw_socket) assert raw_socket.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY) != 0 - @pytest.mark.skipif(not socket.has_ipv6, reason='IPv6 is not available') + @pytest.mark.skipif(not has_ipv6, reason='IPv6 is not available') @pytest.mark.parametrize('local_addr, expected_client_addr', [ pytest.param('', '::1', id='dualstack'), pytest.param('127.0.0.1', '127.0.0.1', id='ipv4'), @@ -227,7 +241,7 @@ @pytest.mark.parametrize('target, exception_class', [ pytest.param( 'localhost', ExceptionGroup, id='multi', - marks=[pytest.mark.skipif(not socket.has_ipv6, reason='IPv6 is not available')] + marks=[pytest.mark.skipif(not has_ipv6, reason='IPv6 is not available')] ), pytest.param('127.0.0.1', ConnectionRefusedError, id='single') ]) @@ -441,9 +455,9 @@ @pytest.mark.parametrize('family', [ pytest.param(AddressFamily.AF_INET, id='ipv4'), pytest.param(AddressFamily.AF_INET6, id='ipv6', - marks=[pytest.mark.skipif(not socket.has_ipv6, reason='no IPv6 support')]), + marks=[pytest.mark.skipif(not has_ipv6, reason='no IPv6 support')]), pytest.param(socket.AF_UNSPEC, id='both', - marks=[pytest.mark.skipif(not socket.has_ipv6, reason='no IPv6 support')]) + marks=[pytest.mark.skipif(not has_ipv6, reason='no IPv6 support')]) ]) async def test_accept(self, family: AnyIPAddressFamily) -> None: async with await create_tcp_listener(local_host='localhost', family=family) as multi: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/tests/test_synchronization.py new/anyio-3.4.0/tests/test_synchronization.py --- old/anyio-3.3.4/tests/test_synchronization.py 2021-10-16 12:45:31.000000000 +0200 +++ new/anyio-3.4.0/tests/test_synchronization.py 2021-11-23 00:57:30.000000000 +0100 @@ -5,7 +5,7 @@ from anyio import ( CancelScope, Condition, Event, Lock, Semaphore, WouldBlock, create_task_group, to_thread, wait_all_tasks_blocked) -from anyio.abc import CapacityLimiter +from anyio.abc import CapacityLimiter, TaskStatus pytestmark = pytest.mark.anyio @@ -65,23 +65,34 @@ assert lock.locked() tg.start_soon(try_lock) - async def test_cancel(self) -> None: - task_started = got_lock = False - - async def task() -> None: - nonlocal task_started, got_lock - task_started = True + @pytest.mark.parametrize('release_first', [ + pytest.param(False, id='releaselast'), + pytest.param(True, id='releasefirst') + ]) + async def test_cancel_during_acquire(self, release_first: bool) -> None: + acquired = False + + async def task(*, task_status: TaskStatus) -> None: + nonlocal acquired + task_status.started() async with lock: - got_lock = True + acquired = True lock = Lock() async with create_task_group() as tg: - async with lock: - tg.start_soon(task) - tg.cancel_scope.cancel() + await lock.acquire() + await tg.start(task) + tg.cancel_scope.cancel() + with CancelScope(shield=True): + if release_first: + lock.release() + await wait_all_tasks_blocked() + else: + await wait_all_tasks_blocked() + lock.release() - assert task_started - assert not got_lock + assert not acquired + assert not lock.locked() async def test_statistics(self) -> None: async def waiter() -> None: @@ -282,24 +293,34 @@ assert semaphore.value == 0 pytest.raises(WouldBlock, semaphore.acquire_nowait) - async def test_acquire_cancel(self) -> None: - local_scope = acquired = None - - async def task() -> None: - nonlocal local_scope, acquired - with CancelScope() as local_scope: - async with semaphore: - acquired = True + @pytest.mark.parametrize('release_first', [ + pytest.param(False, id='releaselast'), + pytest.param(True, id='releasefirst') + ]) + async def test_cancel_during_acquire(self, release_first: bool) -> None: + acquired = False + + async def task(*, task_status: TaskStatus) -> None: + nonlocal acquired + task_status.started() + async with semaphore: + acquired = True semaphore = Semaphore(1) async with create_task_group() as tg: - async with semaphore: - tg.start_soon(task) - await wait_all_tasks_blocked() - assert local_scope is not None - local_scope.cancel() + await semaphore.acquire() + await tg.start(task) + tg.cancel_scope.cancel() + with CancelScope(shield=True): + if release_first: + semaphore.release() + await wait_all_tasks_blocked() + else: + await wait_all_tasks_blocked() + semaphore.release() assert not acquired + assert semaphore.value == 1 @pytest.mark.parametrize('max_value', [2, None]) async def test_max_value(self, max_value: Optional[int]) -> None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/tests/test_taskgroups.py new/anyio-3.4.0/tests/test_taskgroups.py --- old/anyio-3.3.4/tests/test_taskgroups.py 2021-10-16 12:45:31.000000000 +0200 +++ new/anyio-3.4.0/tests/test_taskgroups.py 2021-11-23 00:57:30.000000000 +0100 @@ -2,15 +2,15 @@ import re import sys import time -from typing import Any, AsyncGenerator, Coroutine, Dict, Generator, NoReturn, Set +from typing import Any, AsyncGenerator, Coroutine, Dict, Generator, NoReturn, Optional, Set import pytest -import trio import anyio from anyio import ( CancelScope, ExceptionGroup, create_task_group, current_effective_deadline, current_time, - fail_after, get_cancelled_exc_class, move_on_after, sleep, wait_all_tasks_blocked) + fail_after, get_cancelled_exc_class, get_current_task, move_on_after, sleep, + wait_all_tasks_blocked) from anyio.abc import TaskGroup, TaskStatus from anyio.lowlevel import checkpoint @@ -54,7 +54,7 @@ @pytest.mark.parametrize('module', [ pytest.param(asyncio, id='asyncio'), - pytest.param(trio, id='trio') + pytest.param(pytest.importorskip('trio'), id='trio') ]) def test_run_natively(module: Any) -> None: async def testfunc() -> None: @@ -1004,3 +1004,45 @@ await wait_all_tasks_blocked() assert isinstance(task, asyncio.Task) task.cancel('blah') + + +async def test_start_soon_parent_id() -> None: + root_task_id = get_current_task().id + parent_id: Optional[int] = None + + async def subtask() -> None: + nonlocal parent_id + parent_id = get_current_task().parent_id + + async def starter_task() -> None: + tg.start_soon(subtask) + + async with anyio.create_task_group() as tg: + tg.start_soon(starter_task) + + assert parent_id == root_task_id + + +async def test_start_parent_id() -> None: + root_task_id = get_current_task().id + starter_task_id: Optional[int] = None + initial_parent_id: Optional[int] = None + permanent_parent_id: Optional[int] = None + + async def subtask(*, task_status: TaskStatus) -> None: + nonlocal initial_parent_id, permanent_parent_id + initial_parent_id = get_current_task().parent_id + task_status.started() + permanent_parent_id = get_current_task().parent_id + + async def starter_task() -> None: + nonlocal starter_task_id + starter_task_id = get_current_task().id + await tg.start(subtask) + + async with anyio.create_task_group() as tg: + tg.start_soon(starter_task) + + assert initial_parent_id != permanent_parent_id + assert initial_parent_id == starter_task_id + assert permanent_parent_id == root_task_id diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/anyio-3.3.4/tests/test_to_thread.py new/anyio-3.4.0/tests/test_to_thread.py --- old/anyio-3.3.4/tests/test_to_thread.py 2021-10-16 12:45:31.000000000 +0200 +++ new/anyio-3.4.0/tests/test_to_thread.py 2021-11-23 00:57:30.000000000 +0100 @@ -3,10 +3,12 @@ import threading import time from concurrent.futures import Future +from contextvars import ContextVar from functools import partial from typing import Any, List, NoReturn, Optional import pytest +import sniffio import anyio.to_thread from anyio import ( @@ -132,6 +134,17 @@ assert future.result(1) +async def test_contextvar_propagation() -> None: + var = ContextVar('var', default=1) + var.set(6) + assert await to_thread.run_sync(var.get) == 6 + + +async def test_asynclib_detection() -> None: + with pytest.raises(sniffio.AsyncLibraryNotFoundError): + await to_thread.run_sync(sniffio.current_async_library) + + @pytest.mark.parametrize('anyio_backend', ['asyncio']) async def test_asyncio_cancel_native_task() -> None: task: "Optional[asyncio.Task[None]]" = None
participants (1)
-
Source-Sync