From 96cdeb256b60b13ec6c3f12aefa9e6a616aa5fcc Mon Sep 17 00:00:00 2001 From: Guillermo Rodriguez Date: Tue, 12 May 2020 09:10:16 -0300 Subject: [PATCH 01/23] Added helpful error message in the case the user passed incorrect type of function to both "from_thread_run" functions. --- trio/_threads.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/trio/_threads.py b/trio/_threads.py index 811bc526a0..f9d8664bb8 100644 --- a/trio/_threads.py +++ b/trio/_threads.py @@ -5,6 +5,8 @@ import attr import outcome +import inspect + import trio from ._sync import CapacityLimiter @@ -364,7 +366,7 @@ def from_thread_run(afn, *args, trio_token=None): RuntimeError: if you try calling this from inside the Trio thread, which would otherwise cause a deadlock. AttributeError: if no ``trio_token`` was provided, and we can't infer - one from context. + one from context. Also if ``afn`` is not an async function. **Locating a Trio Token**: There are two ways to specify which `trio.run` loop to reenter: @@ -377,6 +379,9 @@ def from_thread_run(afn, *args, trio_token=None): "foreign" thread, spawned using some other framework, and still want to enter Trio. """ + if not inspect.iscoroutinefunction(afn): + raise AttributeError("afn must be an asynchronous function") + def callback(q, afn, args): @disable_ki_protection async def unprotected_afn(): @@ -409,7 +414,7 @@ def from_thread_run_sync(fn, *args, trio_token=None): RuntimeError: if you try calling this from inside the Trio thread, which would otherwise cause a deadlock. AttributeError: if no ``trio_token`` was provided, and we can't infer - one from context. + one from context. Also if ``fn`` is not a sync function. **Locating a Trio Token**: There are two ways to specify which `trio.run` loop to reenter: @@ -422,6 +427,9 @@ def from_thread_run_sync(fn, *args, trio_token=None): "foreign" thread, spawned using some other framework, and still want to enter Trio. """ + if not (inspect.isfunction(fn) and not inspect.iscoroutinefunction(fn)): + raise AttributeError("fn must be a synchronous function") + def callback(q, fn, args): @disable_ki_protection def unprotected_fn(): From 015b220ad0e7e5f766a04b52705709d611ffb5b0 Mon Sep 17 00:00:00 2001 From: Guillermo Rodriguez Date: Tue, 12 May 2020 23:54:58 -0300 Subject: [PATCH 02/23] Extracted "coroutine or TypeError" logic from Runner.spawn_impl, moved it to trio._util. Used it in trio.from_thread_run. --- trio/_core/_run.py | 85 ++------------------------------------------ trio/_threads.py | 12 +++---- trio/_util.py | 88 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 90 deletions(-) diff --git a/trio/_core/_run.py b/trio/_core/_run.py index 5904e682fd..26da1ed9f0 100644 --- a/trio/_core/_run.py +++ b/trio/_core/_run.py @@ -17,7 +17,6 @@ from sniffio import current_async_library_cvar import attr -from async_generator import isasyncgen from sortedcontainers import SortedDict from outcome import Error, Value, capture @@ -36,7 +35,7 @@ ) from .. import _core from .._deprecate import deprecated -from .._util import Final, NoPublicConstructor +from .._util import Final, NoPublicConstructor, coroutine_or_error _NO_SEND = object() @@ -1243,86 +1242,8 @@ def spawn_impl(self, async_fn, args, nursery, name, *, system_task=False): if nursery is None: assert self.init_task is None - ###### - # Call the function and get the coroutine object, while giving helpful - # errors for common mistakes. - ###### - - def _return_value_looks_like_wrong_library(value): - # Returned by legacy @asyncio.coroutine functions, which includes - # a surprising proportion of asyncio builtins. - if isinstance(value, collections.abc.Generator): - return True - # The protocol for detecting an asyncio Future-like object - if getattr(value, "_asyncio_future_blocking", None) is not None: - return True - # This janky check catches tornado Futures and twisted Deferreds. - # By the time we're calling this function, we already know - # something has gone wrong, so a heuristic is pretty safe. - if value.__class__.__name__ in ("Future", "Deferred"): - return True - return False - - try: - coro = async_fn(*args) - except TypeError: - # Give good error for: nursery.start_soon(trio.sleep(1)) - if isinstance(async_fn, collections.abc.Coroutine): - raise TypeError( - "Trio was expecting an async function, but instead it got " - "a coroutine object {async_fn!r}\n" - "\n" - "Probably you did something like:\n" - "\n" - " trio.run({async_fn.__name__}(...)) # incorrect!\n" - " nursery.start_soon({async_fn.__name__}(...)) # incorrect!\n" - "\n" - "Instead, you want (notice the parentheses!):\n" - "\n" - " trio.run({async_fn.__name__}, ...) # correct!\n" - " nursery.start_soon({async_fn.__name__}, ...) # correct!" - .format(async_fn=async_fn) - ) from None - - # Give good error for: nursery.start_soon(future) - if _return_value_looks_like_wrong_library(async_fn): - raise TypeError( - "Trio was expecting an async function, but instead it got " - "{!r} – are you trying to use a library written for " - "asyncio/twisted/tornado or similar? That won't work " - "without some sort of compatibility shim." - .format(async_fn) - ) from None - - raise - - # We can't check iscoroutinefunction(async_fn), because that will fail - # for things like functools.partial objects wrapping an async - # function. So we have to just call it and then check whether the - # return value is a coroutine object. - if not isinstance(coro, collections.abc.Coroutine): - # Give good error for: nursery.start_soon(func_returning_future) - if _return_value_looks_like_wrong_library(coro): - raise TypeError( - "start_soon got unexpected {!r} – are you trying to use a " - "library written for asyncio/twisted/tornado or similar? " - "That won't work without some sort of compatibility shim." - .format(coro) - ) - - if isasyncgen(coro): - raise TypeError( - "start_soon expected an async function but got an async " - "generator {!r}".format(coro) - ) - - # Give good error for: nursery.start_soon(some_sync_fn) - raise TypeError( - "Trio expected an async function, but {!r} appears to be " - "synchronous".format( - getattr(async_fn, "__qualname__", async_fn) - ) - ) + # Check if async_fn is callable coroutine + coro = coroutine_or_error(async_fn, args) ###### # Set up the Task object diff --git a/trio/_threads.py b/trio/_threads.py index f9d8664bb8..5c2041776c 100644 --- a/trio/_threads.py +++ b/trio/_threads.py @@ -5,12 +5,11 @@ import attr import outcome -import inspect - import trio from ._sync import CapacityLimiter from ._core import enable_ki_protection, disable_ki_protection, RunVar, TrioToken +from ._util import coroutine_or_error # Global due to Threading API, thread local storage for trio token TOKEN_LOCAL = threading.local() @@ -366,7 +365,7 @@ def from_thread_run(afn, *args, trio_token=None): RuntimeError: if you try calling this from inside the Trio thread, which would otherwise cause a deadlock. AttributeError: if no ``trio_token`` was provided, and we can't infer - one from context. Also if ``afn`` is not an async function. + one from context. **Locating a Trio Token**: There are two ways to specify which `trio.run` loop to reenter: @@ -379,13 +378,12 @@ def from_thread_run(afn, *args, trio_token=None): "foreign" thread, spawned using some other framework, and still want to enter Trio. """ - if not inspect.iscoroutinefunction(afn): - raise AttributeError("afn must be an asynchronous function") def callback(q, afn, args): @disable_ki_protection async def unprotected_afn(): - return await afn(*args) + coro = coroutine_or_error(afn, args) + return await coro async def await_in_trio_thread_task(): q.put_nowait(await outcome.acapture(unprotected_afn)) @@ -427,8 +425,6 @@ def from_thread_run_sync(fn, *args, trio_token=None): "foreign" thread, spawned using some other framework, and still want to enter Trio. """ - if not (inspect.isfunction(fn) and not inspect.iscoroutinefunction(fn)): - raise AttributeError("fn must be a synchronous function") def callback(q, fn, args): @disable_ki_protection diff --git a/trio/_util.py b/trio/_util.py index b331c8b48f..0ba286418f 100644 --- a/trio/_util.py +++ b/trio/_util.py @@ -8,6 +8,9 @@ from functools import wraps, update_wrapper import typing as t import threading +import collections + +from async_generator import isasyncgen from ._deprecate import warn_deprecated @@ -85,6 +88,91 @@ def is_main_thread(): return False +###### +# Call the function and get the coroutine object, while giving helpful +# errors for common mistakes. Returns coroutine object. +###### +def coroutine_or_error(async_fn, args): + + def _return_value_looks_like_wrong_library(value): + # Returned by legacy @asyncio.coroutine functions, which includes + # a surprising proportion of asyncio builtins. + if isinstance(value, collections.abc.Generator): + return True + # The protocol for detecting an asyncio Future-like object + if getattr(value, "_asyncio_future_blocking", None) is not None: + return True + # This janky check catches tornado Futures and twisted Deferreds. + # By the time we're calling this function, we already know + # something has gone wrong, so a heuristic is pretty safe. + if value.__class__.__name__ in ("Future", "Deferred"): + return True + return False + + try: + coro = async_fn(*args) + except TypeError: + # Give good error for: nursery.start_soon(trio.sleep(1)) + if isinstance(async_fn, collections.abc.Coroutine): + raise TypeError( + "Trio was expecting an async function, but instead it got " + "a coroutine object {async_fn!r}\n" + "\n" + "Probably you did something like:\n" + "\n" + " trio.run({async_fn.__name__}(...)) # incorrect!\n" + " nursery.start_soon({async_fn.__name__}(...)) # incorrect!\n" + "\n" + "Instead, you want (notice the parentheses!):\n" + "\n" + " trio.run({async_fn.__name__}, ...) # correct!\n" + " nursery.start_soon({async_fn.__name__}, ...) # correct!" + .format(async_fn=async_fn) + ) from None + + # Give good error for: nursery.start_soon(future) + if _return_value_looks_like_wrong_library(async_fn): + raise TypeError( + "Trio was expecting an async function, but instead it got " + "{!r} – are you trying to use a library written for " + "asyncio/twisted/tornado or similar? That won't work " + "without some sort of compatibility shim." + .format(async_fn) + ) from None + + raise + + # We can't check iscoroutinefunction(async_fn), because that will fail + # for things like functools.partial objects wrapping an async + # function. So we have to just call it and then check whether the + # return value is a coroutine object. + if not isinstance(coro, collections.abc.Coroutine): + # Give good error for: nursery.start_soon(func_returning_future) + if _return_value_looks_like_wrong_library(coro): + raise TypeError( + "start_soon got unexpected {!r} – are you trying to use a " + "library written for asyncio/twisted/tornado or similar? " + "That won't work without some sort of compatibility shim." + .format(coro) + ) + + if isasyncgen(coro): + raise TypeError( + "start_soon expected an async function but got an async " + "generator {!r}".format(coro) + ) + + # Give good error for: nursery.start_soon(some_sync_fn) + raise TypeError( + "Trio expected an async function, but {!r} appears to be " + "synchronous".format( + getattr(async_fn, "__qualname__", async_fn) + ) + ) + + return coro + + class ConflictDetector: """Detect when two tasks are about to perform operations that would conflict. From 126f7f4e93117f03b1d8db0d41bebb3453c6b009 Mon Sep 17 00:00:00 2001 From: Guillermo Rodriguez Date: Wed, 13 May 2020 08:37:42 -0300 Subject: [PATCH 03/23] Added sync check --- trio/_threads.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/trio/_threads.py b/trio/_threads.py index 5c2041776c..67d879f495 100644 --- a/trio/_threads.py +++ b/trio/_threads.py @@ -3,6 +3,7 @@ from itertools import count import attr +import inspect import outcome import trio @@ -407,12 +408,13 @@ def from_thread_run_sync(fn, *args, trio_token=None): RunFinishedError: if the corresponding call to `trio.run` has already completed. Cancelled: if the corresponding call to `trio.run` completes - while ``afn(*args)`` is running, then ``afn`` is likely to raise + while ``fn(*args)`` is running, then ``fn`` is likely to raise :exc:`trio.Cancelled`, and this will propagate out into RuntimeError: if you try calling this from inside the Trio thread, which would otherwise cause a deadlock. AttributeError: if no ``trio_token`` was provided, and we can't infer one from context. Also if ``fn`` is not a sync function. + TypeError: if ``fn`` is not callable or is an async function **Locating a Trio Token**: There are two ways to specify which `trio.run` loop to reenter: @@ -426,6 +428,14 @@ def from_thread_run_sync(fn, *args, trio_token=None): to enter Trio. """ + if not callable(fn) or inspect.iscoroutinefunction(fn): + raise TypeError( + "Trio expected a sync function, but {!r} appears to not be " + "callable or asynchronous".format( + getattr(fn, "__qualname__", fn) + ) + ) + def callback(q, fn, args): @disable_ki_protection def unprotected_fn(): From 417ba5b93782d4fa56b97f5ed7531247ba7d1464 Mon Sep 17 00:00:00 2001 From: Guillermo Rodriguez Date: Wed, 13 May 2020 08:42:29 -0300 Subject: [PATCH 04/23] Ran code formatter and added comments. --- trio/_threads.py | 8 +++----- trio/_util.py | 8 ++------ 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/trio/_threads.py b/trio/_threads.py index 67d879f495..b93dc3b5d8 100644 --- a/trio/_threads.py +++ b/trio/_threads.py @@ -367,6 +367,7 @@ def from_thread_run(afn, *args, trio_token=None): which would otherwise cause a deadlock. AttributeError: if no ``trio_token`` was provided, and we can't infer one from context. + TypeError: if ``afn`` is not an asynchronous function. **Locating a Trio Token**: There are two ways to specify which `trio.run` loop to reenter: @@ -379,7 +380,6 @@ def from_thread_run(afn, *args, trio_token=None): "foreign" thread, spawned using some other framework, and still want to enter Trio. """ - def callback(q, afn, args): @disable_ki_protection async def unprotected_afn(): @@ -414,7 +414,7 @@ def from_thread_run_sync(fn, *args, trio_token=None): which would otherwise cause a deadlock. AttributeError: if no ``trio_token`` was provided, and we can't infer one from context. Also if ``fn`` is not a sync function. - TypeError: if ``fn`` is not callable or is an async function + TypeError: if ``fn`` is not callable or is an async function. **Locating a Trio Token**: There are two ways to specify which `trio.run` loop to reenter: @@ -431,9 +431,7 @@ def from_thread_run_sync(fn, *args, trio_token=None): if not callable(fn) or inspect.iscoroutinefunction(fn): raise TypeError( "Trio expected a sync function, but {!r} appears to not be " - "callable or asynchronous".format( - getattr(fn, "__qualname__", fn) - ) + "callable or asynchronous".format(getattr(fn, "__qualname__", fn)) ) def callback(q, fn, args): diff --git a/trio/_util.py b/trio/_util.py index 0ba286418f..f5733ad85e 100644 --- a/trio/_util.py +++ b/trio/_util.py @@ -93,7 +93,6 @@ def is_main_thread(): # errors for common mistakes. Returns coroutine object. ###### def coroutine_or_error(async_fn, args): - def _return_value_looks_like_wrong_library(value): # Returned by legacy @asyncio.coroutine functions, which includes # a surprising proportion of asyncio builtins. @@ -136,8 +135,7 @@ def _return_value_looks_like_wrong_library(value): "Trio was expecting an async function, but instead it got " "{!r} – are you trying to use a library written for " "asyncio/twisted/tornado or similar? That won't work " - "without some sort of compatibility shim." - .format(async_fn) + "without some sort of compatibility shim.".format(async_fn) ) from None raise @@ -165,9 +163,7 @@ def _return_value_looks_like_wrong_library(value): # Give good error for: nursery.start_soon(some_sync_fn) raise TypeError( "Trio expected an async function, but {!r} appears to be " - "synchronous".format( - getattr(async_fn, "__qualname__", async_fn) - ) + "synchronous".format(getattr(async_fn, "__qualname__", async_fn)) ) return coro From dc06cad0ad2d7ae45f388713530af7f6cd048836 Mon Sep 17 00:00:00 2001 From: Guillermo Rodriguez Date: Wed, 13 May 2020 16:17:44 -0300 Subject: [PATCH 05/23] Added coroutine_or_error test. --- trio/tests/test_util.py | 67 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/trio/tests/test_util.py b/trio/tests/test_util.py index d57b1997df..8e93e7c34d 100644 --- a/trio/tests/test_util.py +++ b/trio/tests/test_util.py @@ -1,12 +1,13 @@ import signal - +import warnings import pytest import trio from .. import _core from .._util import ( - signal_raise, ConflictDetector, is_main_thread, generic_function, Final, - NoPublicConstructor, SubclassingDeprecatedIn_v0_15_0 + signal_raise, ConflictDetector, is_main_thread, coroutine_or_error, + generic_function, Final, NoPublicConstructor, + SubclassingDeprecatedIn_v0_15_0 ) from ..testing import wait_all_tasks_blocked @@ -82,6 +83,66 @@ def not_main_thread(): await trio.to_thread.run_sync(not_main_thread) +async def test_coroutine_or_error(): + + # error for: nursery.start_soon(trio.sleep(1)) + warnings.filterwarnings("error") + + async def test_afunc(): + pass + + with pytest.raises(TypeError): + try: + coroutine_or_error(test_afunc(), []) + except RuntimeWarning: + pass + + # error for: nursery.start_soon(future) + + # legacy @asyncio.coroutine functions + def test_generator(): + yield None + + with pytest.raises(TypeError): + coroutine_or_error(test_generator, []) + + # asyncio Future-like object + class AsycioFutureLike: + def __init__(self): + self._asyncio_future_blocking = "im a value!" + + with pytest.raises(TypeError): + coroutine_or_error(AsycioFutureLike(), []) + + # tornado Futures + class Future: + pass + + with pytest.raises(TypeError): + coroutine_or_error(Future(), []) + + # twisted Deferreds + class Deferreds: + pass + + with pytest.raises(TypeError): + coroutine_or_error(Deferreds(), []) + + # async generator + async def test_agenerator(): + yield None + + with pytest.raises(TypeError): + coroutine_or_error(test_agenerator, []) + + # synchronous function + def test_fn(): + pass + + with pytest.raises(TypeError): + coroutine_or_error(test_fn, []) + + def test_generic_function(): @generic_function def test_func(arg): From dc1e5b8f7220097f1d5f2f87346d7e9796075b32 Mon Sep 17 00:00:00 2001 From: Guillermo Rodriguez Date: Wed, 13 May 2020 16:46:41 -0300 Subject: [PATCH 06/23] Added TypeError tests to from_thread_run and from_thread_run_sync --- trio/tests/test_threads.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/trio/tests/test_threads.py b/trio/tests/test_threads.py index 29d44adc4a..e937cb66e1 100644 --- a/trio/tests/test_threads.py +++ b/trio/tests/test_threads.py @@ -471,6 +471,13 @@ def thread_fn(): trio_time = await to_thread_run_sync(thread_fn) assert isinstance(trio_time, float) + # Test correct error when passed async function + async def async_fn(): + pass + + with pytest.raises(TypeError): + from_thread_run_sync(async_fn) + async def test_trio_from_thread_run(): # Test that to_thread_run_sync correctly "hands off" the trio token to @@ -488,6 +495,16 @@ def thread_fn(): await to_thread_run_sync(thread_fn) assert record == ["in thread", "back in trio"] + # Test correct error when passed sync function + def sync_fn(): + pass + + def thread_fn(): + from_thread_run(sync_fn) + + with pytest.raises(TypeError): + await to_thread_run_sync(thread_fn) + async def test_trio_from_thread_token(): # Test that to_thread_run_sync and spawned trio.from_thread.run_sync() From 5e4145decca7edaf29ce344ecfd7bbd6635413d4 Mon Sep 17 00:00:00 2001 From: Guillermo Rodriguez Date: Wed, 13 May 2020 17:39:47 -0300 Subject: [PATCH 07/23] Moved comprehensive test of callable from test_run.py to test_util.py --- trio/_core/tests/test_run.py | 68 ------------------------ trio/_util.py | 2 +- trio/tests/test_util.py | 100 +++++++++++++++++++++-------------- 3 files changed, 60 insertions(+), 110 deletions(-) diff --git a/trio/_core/tests/test_run.py b/trio/_core/tests/test_run.py index e705af5c22..880b89369e 100644 --- a/trio/_core/tests/test_run.py +++ b/trio/_core/tests/test_run.py @@ -1696,74 +1696,6 @@ async def test_current_effective_deadline(mock_clock): assert _core.current_effective_deadline() == inf -# @coroutine is deprecated since python 3.8, which is fine with us. -@pytest.mark.filterwarnings("ignore:.*@coroutine.*:DeprecationWarning") -def test_nice_error_on_bad_calls_to_run_or_spawn(): - def bad_call_run(*args): - _core.run(*args) - - def bad_call_spawn(*args): - async def main(): - async with _core.open_nursery() as nursery: - nursery.start_soon(*args) - - _core.run(main) - - class Deferred: - "Just kidding" - - with ignore_coroutine_never_awaited_warnings(): - for bad_call in bad_call_run, bad_call_spawn: - - async def f(): # pragma: no cover - pass - - with pytest.raises(TypeError) as excinfo: - bad_call(f()) - assert "expecting an async function" in str(excinfo.value) - - import asyncio - - @asyncio.coroutine - def generator_based_coro(): # pragma: no cover - yield from asyncio.sleep(1) - - with pytest.raises(TypeError) as excinfo: - bad_call(generator_based_coro()) - assert "asyncio" in str(excinfo.value) - - with pytest.raises(TypeError) as excinfo: - bad_call(asyncio.Future()) - assert "asyncio" in str(excinfo.value) - - with pytest.raises(TypeError) as excinfo: - bad_call(lambda: asyncio.Future()) - assert "asyncio" in str(excinfo.value) - - with pytest.raises(TypeError) as excinfo: - bad_call(Deferred()) - assert "twisted" in str(excinfo.value) - - with pytest.raises(TypeError) as excinfo: - bad_call(lambda: Deferred()) - assert "twisted" in str(excinfo.value) - - with pytest.raises(TypeError) as excinfo: - bad_call(len, [1, 2, 3]) - assert "appears to be synchronous" in str(excinfo.value) - - async def async_gen(arg): # pragma: no cover - yield - - with pytest.raises(TypeError) as excinfo: - bad_call(async_gen, 0) - msg = "expected an async function but got an async generator" - assert msg in str(excinfo.value) - - # Make sure no references are kept around to keep anything alive - del excinfo - - def test_calling_asyncio_function_gives_nice_error(): async def child_xyzzy(): import asyncio diff --git a/trio/_util.py b/trio/_util.py index f5733ad85e..ac1a87e41b 100644 --- a/trio/_util.py +++ b/trio/_util.py @@ -92,7 +92,7 @@ def is_main_thread(): # Call the function and get the coroutine object, while giving helpful # errors for common mistakes. Returns coroutine object. ###### -def coroutine_or_error(async_fn, args): +def coroutine_or_error(async_fn, args=[]): def _return_value_looks_like_wrong_library(value): # Returned by legacy @asyncio.coroutine functions, which includes # a surprising proportion of asyncio builtins. diff --git a/trio/tests/test_util.py b/trio/tests/test_util.py index 8e93e7c34d..c09060292e 100644 --- a/trio/tests/test_util.py +++ b/trio/tests/test_util.py @@ -1,9 +1,11 @@ import signal import warnings import pytest +from contextlib import contextmanager import trio from .. import _core +from .._core.tests.tutil import gc_collect_harder from .._util import ( signal_raise, ConflictDetector, is_main_thread, coroutine_or_error, generic_function, Final, NoPublicConstructor, @@ -83,64 +85,80 @@ def not_main_thread(): await trio.to_thread.run_sync(not_main_thread) -async def test_coroutine_or_error(): +# Some of our tests need to leak coroutines, and thus trigger the +# "RuntimeWarning: coroutine '...' was never awaited" message. This context +# manager should be used anywhere this happens to hide those messages, because +# when expected they're clutter. +@contextmanager +def ignore_coroutine_never_awaited_warnings(): + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", message="coroutine '.*' was never awaited" + ) + try: + yield + finally: + # Make sure to trigger any coroutine __del__ methods now, before + # we leave the context manager. + gc_collect_harder() - # error for: nursery.start_soon(trio.sleep(1)) - warnings.filterwarnings("error") - async def test_afunc(): - pass +# @coroutine is deprecated since python 3.8, which is fine with us. +@pytest.mark.filterwarnings("ignore:.*@coroutine.*:DeprecationWarning") +def test_coroutine_or_error(): + class Deferred: + "Just kidding" - with pytest.raises(TypeError): - try: - coroutine_or_error(test_afunc(), []) - except RuntimeWarning: + with ignore_coroutine_never_awaited_warnings(): + + async def f(): # pragma: no cover pass - # error for: nursery.start_soon(future) + with pytest.raises(TypeError) as excinfo: + coroutine_or_error(f()) + assert "expecting an async function" in str(excinfo.value) - # legacy @asyncio.coroutine functions - def test_generator(): - yield None + import asyncio - with pytest.raises(TypeError): - coroutine_or_error(test_generator, []) + @asyncio.coroutine + def generator_based_coro(): # pragma: no cover + yield from asyncio.sleep(1) - # asyncio Future-like object - class AsycioFutureLike: - def __init__(self): - self._asyncio_future_blocking = "im a value!" + with pytest.raises(TypeError) as excinfo: + coroutine_or_error(generator_based_coro()) + assert "asyncio" in str(excinfo.value) - with pytest.raises(TypeError): - coroutine_or_error(AsycioFutureLike(), []) + with pytest.raises(TypeError) as excinfo: + coroutine_or_error(asyncio.Future()) + assert "asyncio" in str(excinfo.value) - # tornado Futures - class Future: - pass + with pytest.raises(TypeError) as excinfo: + coroutine_or_error(lambda: asyncio.Future()) + assert "asyncio" in str(excinfo.value) - with pytest.raises(TypeError): - coroutine_or_error(Future(), []) + with pytest.raises(TypeError) as excinfo: + coroutine_or_error(Deferred()) + assert "twisted" in str(excinfo.value) - # twisted Deferreds - class Deferreds: - pass + with pytest.raises(TypeError) as excinfo: + coroutine_or_error(lambda: Deferred()) + assert "twisted" in str(excinfo.value) - with pytest.raises(TypeError): - coroutine_or_error(Deferreds(), []) + with pytest.raises(TypeError) as excinfo: + coroutine_or_error(len, [[1, 2, 3]]) - # async generator - async def test_agenerator(): - yield None + assert "appears to be synchronous" in str(excinfo.value) - with pytest.raises(TypeError): - coroutine_or_error(test_agenerator, []) + async def async_gen(arg): # pragma: no cover + yield - # synchronous function - def test_fn(): - pass + with pytest.raises(TypeError) as excinfo: + coroutine_or_error(async_gen, [0]) + msg = "expected an async function but got an async generator" + assert msg in str(excinfo.value) - with pytest.raises(TypeError): - coroutine_or_error(test_fn, []) + # Make sure no references are kept around to keep anything alive + del excinfo def test_generic_function(): From c74a829e8c38795f89cb2bbc472e2f8880e69663 Mon Sep 17 00:00:00 2001 From: Guillermo Rodriguez Date: Wed, 13 May 2020 21:15:12 -0300 Subject: [PATCH 08/23] Added error message check --- trio/tests/test_threads.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/trio/tests/test_threads.py b/trio/tests/test_threads.py index e937cb66e1..ed53a13e01 100644 --- a/trio/tests/test_threads.py +++ b/trio/tests/test_threads.py @@ -475,9 +475,11 @@ def thread_fn(): async def async_fn(): pass - with pytest.raises(TypeError): + with pytest.raises(TypeError) as excinfo: from_thread_run_sync(async_fn) + assert "expected a sync function" in str(excinfo.value) + async def test_trio_from_thread_run(): # Test that to_thread_run_sync correctly "hands off" the trio token to @@ -502,9 +504,11 @@ def sync_fn(): def thread_fn(): from_thread_run(sync_fn) - with pytest.raises(TypeError): + with pytest.raises(TypeError) as excinfo: await to_thread_run_sync(thread_fn) + assert "appears to be synchronous" in str(excinfo.value) + async def test_trio_from_thread_token(): # Test that to_thread_run_sync and spawned trio.from_thread.run_sync() From ad2733845cbaad79d294f57dab21d9a1183fcb08 Mon Sep 17 00:00:00 2001 From: Guillermo Rodriguez Date: Wed, 13 May 2020 22:30:11 -0300 Subject: [PATCH 09/23] Marked some function stubs with #pragma no cover --- trio/tests/test_threads.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/trio/tests/test_threads.py b/trio/tests/test_threads.py index ed53a13e01..1c9c5e59bc 100644 --- a/trio/tests/test_threads.py +++ b/trio/tests/test_threads.py @@ -472,7 +472,7 @@ def thread_fn(): assert isinstance(trio_time, float) # Test correct error when passed async function - async def async_fn(): + async def async_fn(): # pragma: no cover pass with pytest.raises(TypeError) as excinfo: @@ -498,7 +498,7 @@ def thread_fn(): assert record == ["in thread", "back in trio"] # Test correct error when passed sync function - def sync_fn(): + def sync_fn(): # pragma: no cover pass def thread_fn(): From 57c10809f8275ed02c478305d72bb42f45f5d582 Mon Sep 17 00:00:00 2001 From: Guillermo Rodriguez Date: Fri, 15 May 2020 17:48:49 -0300 Subject: [PATCH 10/23] Fixed comment on spawn_impl. Moved ignore_coroutine_never_awaited_warnings to tutil. Added raised error test to nursery.start_soon & _core.run. coroutine_or_error now takes variable arguments. Added RuntimeWarning protection to from_thread_run_sync & coroutine_or_error. Fixed typos in comments. --- trio/_core/_run.py | 7 ++--- trio/_core/tests/test_run.py | 56 ++++++++++++++++++++++++------------ trio/_core/tests/tutil.py | 20 +++++++++++++ trio/_threads.py | 23 +++++++-------- trio/_util.py | 9 ++++-- trio/tests/test_threads.py | 16 +++++------ trio/tests/test_util.py | 22 +------------- 7 files changed, 86 insertions(+), 67 deletions(-) diff --git a/trio/_core/_run.py b/trio/_core/_run.py index 26da1ed9f0..02ee8621ab 100644 --- a/trio/_core/_run.py +++ b/trio/_core/_run.py @@ -1242,12 +1242,11 @@ def spawn_impl(self, async_fn, args, nursery, name, *, system_task=False): if nursery is None: assert self.init_task is None - # Check if async_fn is callable coroutine - coro = coroutine_or_error(async_fn, args) - ###### - # Set up the Task object + # Call the function and get the coroutine object, while giving helpful + # errors for common mistakes. ###### + coro = coroutine_or_error(async_fn, *args) if name is None: name = async_fn diff --git a/trio/_core/tests/test_run.py b/trio/_core/tests/test_run.py index 880b89369e..118b046fb5 100644 --- a/trio/_core/tests/test_run.py +++ b/trio/_core/tests/test_run.py @@ -15,7 +15,11 @@ import sniffio import pytest -from .tutil import slow, check_sequence_matches, gc_collect_harder +from .tutil import ( + slow, check_sequence_matches, gc_collect_harder, + ignore_coroutine_never_awaited_warnings +) + from ... import _core from ..._threads import to_thread_run_sync from ..._timeouts import sleep, fail_after @@ -33,24 +37,6 @@ async def sleep_forever(): return await _core.wait_task_rescheduled(lambda _: _core.Abort.SUCCEEDED) -# Some of our tests need to leak coroutines, and thus trigger the -# "RuntimeWarning: coroutine '...' was never awaited" message. This context -# manager should be used anywhere this happens to hide those messages, because -# when expected they're clutter. -@contextmanager -def ignore_coroutine_never_awaited_warnings(): - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", message="coroutine '.*' was never awaited" - ) - try: - yield - finally: - # Make sure to trigger any coroutine __del__ methods now, before - # we leave the context manager. - gc_collect_harder() - - def test_basic(): async def trivial(x): return x @@ -1696,6 +1682,38 @@ async def test_current_effective_deadline(mock_clock): assert _core.current_effective_deadline() == inf +def test_nice_error_on_bad_calls_to_run_or_spawn(): + def bad_call_run(*args): + _core.run(*args) + + def bad_call_spawn(*args): + async def main(): + async with _core.open_nursery() as nursery: + nursery.start_soon(*args) + + _core.run(main) + + for bad_call in bad_call_run, bad_call_spawn: + + async def f(): # pragma: no cover + pass + + with ignore_coroutine_never_awaited_warnings(): + with pytest.raises(TypeError, match="expecting an async function"): + + bad_call(f()) + + async def async_gen(arg): # pragma: no cover + yield arg + + with pytest.raises( + TypeError, + match="expected an async function but got an async generator" + ): + + bad_call(async_gen, 0) + + def test_calling_asyncio_function_gives_nice_error(): async def child_xyzzy(): import asyncio diff --git a/trio/_core/tests/tutil.py b/trio/_core/tests/tutil.py index dac53b81fd..ac090cb8de 100644 --- a/trio/_core/tests/tutil.py +++ b/trio/_core/tests/tutil.py @@ -3,6 +3,8 @@ import os import pytest +import warnings +from contextlib import contextmanager import gc @@ -52,6 +54,24 @@ def gc_collect_harder(): gc.collect() +# Some of our tests need to leak coroutines, and thus trigger the +# "RuntimeWarning: coroutine '...' was never awaited" message. This context +# manager should be used anywhere this happens to hide those messages, because +# when expected they're clutter. +@contextmanager +def ignore_coroutine_never_awaited_warnings(): + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", message="coroutine '.*' was never awaited" + ) + try: + yield + finally: + # Make sure to trigger any coroutine __del__ methods now, before + # we leave the context manager. + gc_collect_harder() + + # template is like: # [1, {2.1, 2.2}, 3] -> matches [1, 2.1, 2.2, 3] or [1, 2.2, 2.1, 3] def check_sequence_matches(seq, template): diff --git a/trio/_threads.py b/trio/_threads.py index b93dc3b5d8..142d7005ac 100644 --- a/trio/_threads.py +++ b/trio/_threads.py @@ -383,7 +383,7 @@ def from_thread_run(afn, *args, trio_token=None): def callback(q, afn, args): @disable_ki_protection async def unprotected_afn(): - coro = coroutine_or_error(afn, args) + coro = coroutine_or_error(afn, *args) return await coro async def await_in_trio_thread_task(): @@ -407,9 +407,6 @@ def from_thread_run_sync(fn, *args, trio_token=None): Raises: RunFinishedError: if the corresponding call to `trio.run` has already completed. - Cancelled: if the corresponding call to `trio.run` completes - while ``fn(*args)`` is running, then ``fn`` is likely to raise - :exc:`trio.Cancelled`, and this will propagate out into RuntimeError: if you try calling this from inside the Trio thread, which would otherwise cause a deadlock. AttributeError: if no ``trio_token`` was provided, and we can't infer @@ -427,17 +424,19 @@ def from_thread_run_sync(fn, *args, trio_token=None): "foreign" thread, spawned using some other framework, and still want to enter Trio. """ - - if not callable(fn) or inspect.iscoroutinefunction(fn): - raise TypeError( - "Trio expected a sync function, but {!r} appears to not be " - "callable or asynchronous".format(getattr(fn, "__qualname__", fn)) - ) - def callback(q, fn, args): @disable_ki_protection def unprotected_fn(): - return fn(*args) + call = fn(*args) + + if inspect.iscoroutine(call): + call.close() + raise TypeError( + "Trio expected a sync function, but {!r} appears to be " + "asynchronous".format(getattr(fn, "__qualname__", fn)) + ) + + return call res = outcome.capture(unprotected_fn) q.put_nowait(res) diff --git a/trio/_util.py b/trio/_util.py index ac1a87e41b..5cc9737d22 100644 --- a/trio/_util.py +++ b/trio/_util.py @@ -92,7 +92,7 @@ def is_main_thread(): # Call the function and get the coroutine object, while giving helpful # errors for common mistakes. Returns coroutine object. ###### -def coroutine_or_error(async_fn, args=[]): +def coroutine_or_error(async_fn, *args): def _return_value_looks_like_wrong_library(value): # Returned by legacy @asyncio.coroutine functions, which includes # a surprising proportion of asyncio builtins. @@ -110,9 +110,14 @@ def _return_value_looks_like_wrong_library(value): try: coro = async_fn(*args) + except TypeError: # Give good error for: nursery.start_soon(trio.sleep(1)) if isinstance(async_fn, collections.abc.Coroutine): + + # explicitly close coroutine to avoid RuntimeWarning + async_fn.close() + raise TypeError( "Trio was expecting an async function, but instead it got " "a coroutine object {async_fn!r}\n" @@ -148,7 +153,7 @@ def _return_value_looks_like_wrong_library(value): # Give good error for: nursery.start_soon(func_returning_future) if _return_value_looks_like_wrong_library(coro): raise TypeError( - "start_soon got unexpected {!r} – are you trying to use a " + "Trio got unexpected {!r} – are you trying to use a " "library written for asyncio/twisted/tornado or similar? " "That won't work without some sort of compatibility shim." .format(coro) diff --git a/trio/tests/test_threads.py b/trio/tests/test_threads.py index 1c9c5e59bc..8f92d94d65 100644 --- a/trio/tests/test_threads.py +++ b/trio/tests/test_threads.py @@ -13,7 +13,7 @@ ) from .._core.tests.test_ki import ki_self -from .._core.tests.tutil import slow +from .._core.tests.tutil import ignore_coroutine_never_awaited_warnings async def test_do_in_trio_thread(): @@ -475,10 +475,12 @@ def thread_fn(): async def async_fn(): # pragma: no cover pass - with pytest.raises(TypeError) as excinfo: + def thread_fn(): from_thread_run_sync(async_fn) - assert "expected a sync function" in str(excinfo.value) + with pytest.raises(TypeError, match="expected a sync function"): + + await to_thread_run_sync(thread_fn) async def test_trio_from_thread_run(): @@ -501,13 +503,9 @@ def thread_fn(): def sync_fn(): # pragma: no cover pass - def thread_fn(): - from_thread_run(sync_fn) - - with pytest.raises(TypeError) as excinfo: - await to_thread_run_sync(thread_fn) + with pytest.raises(TypeError, match="appears to be synchronous"): - assert "appears to be synchronous" in str(excinfo.value) + await to_thread_run_sync(from_thread_run, sync_fn) async def test_trio_from_thread_token(): diff --git a/trio/tests/test_util.py b/trio/tests/test_util.py index c09060292e..009f9fa8f7 100644 --- a/trio/tests/test_util.py +++ b/trio/tests/test_util.py @@ -1,11 +1,9 @@ import signal -import warnings import pytest -from contextlib import contextmanager import trio from .. import _core -from .._core.tests.tutil import gc_collect_harder +from .._core.tests.tutil import ignore_coroutine_never_awaited_warnings from .._util import ( signal_raise, ConflictDetector, is_main_thread, coroutine_or_error, generic_function, Final, NoPublicConstructor, @@ -85,24 +83,6 @@ def not_main_thread(): await trio.to_thread.run_sync(not_main_thread) -# Some of our tests need to leak coroutines, and thus trigger the -# "RuntimeWarning: coroutine '...' was never awaited" message. This context -# manager should be used anywhere this happens to hide those messages, because -# when expected they're clutter. -@contextmanager -def ignore_coroutine_never_awaited_warnings(): - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", message="coroutine '.*' was never awaited" - ) - try: - yield - finally: - # Make sure to trigger any coroutine __del__ methods now, before - # we leave the context manager. - gc_collect_harder() - - # @coroutine is deprecated since python 3.8, which is fine with us. @pytest.mark.filterwarnings("ignore:.*@coroutine.*:DeprecationWarning") def test_coroutine_or_error(): From f4c11e9dacabbc62588fbf0ef1fe71d7a0cf630b Mon Sep 17 00:00:00 2001 From: Guillermo Rodriguez Date: Sat, 16 May 2020 09:02:30 -0300 Subject: [PATCH 11/23] Fixed/added some comments. Removed unnecessary ignore_coroutine_never_awaited_warnings. Better naming of return value in from_thread_run_sync. --- trio/_core/_run.py | 3 +++ trio/_core/tests/test_run.py | 6 ++---- trio/_threads.py | 12 +++++++----- trio/tests/test_threads.py | 1 - 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/trio/_core/_run.py b/trio/_core/_run.py index 02ee8621ab..f77860d2f2 100644 --- a/trio/_core/_run.py +++ b/trio/_core/_run.py @@ -1273,6 +1273,9 @@ async def python_wrapper(orig_coro): LOCALS_KEY_KI_PROTECTION_ENABLED, system_task ) + ###### + # Set up the Task object + ###### task = Task._create( coro=coro, parent_nursery=nursery, diff --git a/trio/_core/tests/test_run.py b/trio/_core/tests/test_run.py index 118b046fb5..6c08c685f3 100644 --- a/trio/_core/tests/test_run.py +++ b/trio/_core/tests/test_run.py @@ -1698,10 +1698,9 @@ async def main(): async def f(): # pragma: no cover pass - with ignore_coroutine_never_awaited_warnings(): - with pytest.raises(TypeError, match="expecting an async function"): + with pytest.raises(TypeError, match="expecting an async function"): - bad_call(f()) + bad_call(f()) async def async_gen(arg): # pragma: no cover yield arg @@ -1710,7 +1709,6 @@ async def async_gen(arg): # pragma: no cover TypeError, match="expected an async function but got an async generator" ): - bad_call(async_gen, 0) diff --git a/trio/_threads.py b/trio/_threads.py index 142d7005ac..ffd85812cf 100644 --- a/trio/_threads.py +++ b/trio/_threads.py @@ -411,7 +411,7 @@ def from_thread_run_sync(fn, *args, trio_token=None): which would otherwise cause a deadlock. AttributeError: if no ``trio_token`` was provided, and we can't infer one from context. Also if ``fn`` is not a sync function. - TypeError: if ``fn`` is not callable or is an async function. + TypeError: if ``fn`` is an async function. **Locating a Trio Token**: There are two ways to specify which `trio.run` loop to reenter: @@ -427,16 +427,18 @@ def from_thread_run_sync(fn, *args, trio_token=None): def callback(q, fn, args): @disable_ki_protection def unprotected_fn(): - call = fn(*args) + ret = fn(*args) - if inspect.iscoroutine(call): - call.close() + if inspect.iscoroutine(ret): + + # Manually close coroutine to avoid RuntimeWarnings + ret.close() raise TypeError( "Trio expected a sync function, but {!r} appears to be " "asynchronous".format(getattr(fn, "__qualname__", fn)) ) - return call + return ret res = outcome.capture(unprotected_fn) q.put_nowait(res) diff --git a/trio/tests/test_threads.py b/trio/tests/test_threads.py index 8f92d94d65..545986a795 100644 --- a/trio/tests/test_threads.py +++ b/trio/tests/test_threads.py @@ -13,7 +13,6 @@ ) from .._core.tests.test_ki import ki_self -from .._core.tests.tutil import ignore_coroutine_never_awaited_warnings async def test_do_in_trio_thread(): From 28b08d1a40b83d3a478d9ac9315b7e3bc5af4f8d Mon Sep 17 00:00:00 2001 From: Guillermo Rodriguez Date: Mon, 18 May 2020 09:17:49 -0300 Subject: [PATCH 12/23] Fixed a comment and other formatting issues. --- trio/_core/tests/test_run.py | 1 - trio/_threads.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/trio/_core/tests/test_run.py b/trio/_core/tests/test_run.py index 6c08c685f3..53446a601f 100644 --- a/trio/_core/tests/test_run.py +++ b/trio/_core/tests/test_run.py @@ -1699,7 +1699,6 @@ async def f(): # pragma: no cover pass with pytest.raises(TypeError, match="expecting an async function"): - bad_call(f()) async def async_gen(arg): # pragma: no cover diff --git a/trio/_threads.py b/trio/_threads.py index ffd85812cf..c03a353789 100644 --- a/trio/_threads.py +++ b/trio/_threads.py @@ -410,7 +410,7 @@ def from_thread_run_sync(fn, *args, trio_token=None): RuntimeError: if you try calling this from inside the Trio thread, which would otherwise cause a deadlock. AttributeError: if no ``trio_token`` was provided, and we can't infer - one from context. Also if ``fn`` is not a sync function. + one from context. TypeError: if ``fn`` is an async function. **Locating a Trio Token**: There are two ways to specify which @@ -430,7 +430,6 @@ def unprotected_fn(): ret = fn(*args) if inspect.iscoroutine(ret): - # Manually close coroutine to avoid RuntimeWarnings ret.close() raise TypeError( From 0520ffad09bfbe2bbb6f0d8092a95353dad4d477 Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Tue, 19 May 2020 09:34:06 +0400 Subject: [PATCH 13/23] Simplify releasing notes --- docs/source/releasing.rst | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/docs/source/releasing.rst b/docs/source/releasing.rst index 0ebad58a80..98b53406f2 100644 --- a/docs/source/releasing.rst +++ b/docs/source/releasing.rst @@ -19,7 +19,7 @@ Things to do for releasing: * Do the actual release changeset - + update version number + + bump version number - increment as per Semantic Versioning rules @@ -37,31 +37,18 @@ Things to do for releasing: * create pull request to ``python-trio/trio``'s "master" branch -* announce PR on gitter - - + wait for feedback - - + fix problems, if any - * verify that all checks succeeded -* acknowledge the release PR - - + or rather, somebody else should do that - -* tag with vVERSION +* tag with vVERSION, push tag * push to PyPI + ``python3 setup.py sdist bdist_wheel upload`` -* announce on gitter - -* update version number +* update version number in the same pull request + add ``+dev`` tag to the end -* prepare another pull request to "master" - - + acknowledge it +* approve and merge the release pull request +* announce on gitter From 5cec84eb5b4bcc66c8cc4855656f0acf92cb8aad Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Tue, 19 May 2020 14:04:19 +0400 Subject: [PATCH 14/23] docs: improve releasing docs There's no need to approve your own PR, and it's safer to use git clean and twine. --- docs/source/releasing.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/source/releasing.rst b/docs/source/releasing.rst index 98b53406f2..f39aaf274f 100644 --- a/docs/source/releasing.rst +++ b/docs/source/releasing.rst @@ -41,14 +41,16 @@ Things to do for releasing: * tag with vVERSION, push tag -* push to PyPI +* push to PyPI:: - + ``python3 setup.py sdist bdist_wheel upload`` + git clean -xdf # maybe run 'git clean -xdn' first to see what it will delete + python3 setup.py sdist bdist_wheel + twine upload dist/* * update version number in the same pull request + add ``+dev`` tag to the end -* approve and merge the release pull request +* merge the release pull request * announce on gitter From 0f809d0ccfdfb5a94a8ee0311e91737a54eff9d8 Mon Sep 17 00:00:00 2001 From: Joshua Oreman Date: Tue, 19 May 2020 17:17:53 -0700 Subject: [PATCH 15/23] Remove another extra blank line --- trio/_util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/trio/_util.py b/trio/_util.py index 5cc9737d22..06c3a29a19 100644 --- a/trio/_util.py +++ b/trio/_util.py @@ -114,7 +114,6 @@ def _return_value_looks_like_wrong_library(value): except TypeError: # Give good error for: nursery.start_soon(trio.sleep(1)) if isinstance(async_fn, collections.abc.Coroutine): - # explicitly close coroutine to avoid RuntimeWarning async_fn.close() From 7a71a851e3cc8209dc85100d3e5548e2ba7dd268 Mon Sep 17 00:00:00 2001 From: Joshua Oreman Date: Tue, 19 May 2020 17:18:50 -0700 Subject: [PATCH 16/23] Remove a couple more of the extra added blank lines --- trio/tests/test_threads.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/trio/tests/test_threads.py b/trio/tests/test_threads.py index 545986a795..6f5d2b6229 100644 --- a/trio/tests/test_threads.py +++ b/trio/tests/test_threads.py @@ -478,7 +478,6 @@ def thread_fn(): from_thread_run_sync(async_fn) with pytest.raises(TypeError, match="expected a sync function"): - await to_thread_run_sync(thread_fn) @@ -503,7 +502,6 @@ def sync_fn(): # pragma: no cover pass with pytest.raises(TypeError, match="appears to be synchronous"): - await to_thread_run_sync(from_thread_run, sync_fn) From 7de4c84188abf80c463f367504306292b584a7a3 Mon Sep 17 00:00:00 2001 From: Guillermo Rodriguez Date: Tue, 19 May 2020 23:21:07 -0300 Subject: [PATCH 17/23] Added newsfragment --- newsfragments/1244.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/1244.bugfix.rst diff --git a/newsfragments/1244.bugfix.rst b/newsfragments/1244.bugfix.rst new file mode 100644 index 0000000000..d38868f8d9 --- /dev/null +++ b/newsfragments/1244.bugfix.rst @@ -0,0 +1 @@ +Added helpful error message in the case the user passed incorrect type of function to "from_thread_run" & "from_thread_run_sync" functions. From f77c2009e5a5e7a1c0e692609316273bef89f931 Mon Sep 17 00:00:00 2001 From: Guillermo Rodriguez Date: Wed, 20 May 2020 08:00:11 -0300 Subject: [PATCH 18/23] Update newsfragments/1244.bugfix.rst Co-authored-by: Joshua Oreman --- newsfragments/1244.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newsfragments/1244.bugfix.rst b/newsfragments/1244.bugfix.rst index d38868f8d9..6245199a2b 100644 --- a/newsfragments/1244.bugfix.rst +++ b/newsfragments/1244.bugfix.rst @@ -1 +1 @@ -Added helpful error message in the case the user passed incorrect type of function to "from_thread_run" & "from_thread_run_sync" functions. +Added a helpful error message if an async function is passed to `trio.from_thread.run_sync` or a sync function to `trio.from_thread.run`. From 630ff02b4634ddf5bf959b5015625959c74aa0d4 Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Fri, 22 May 2020 09:29:33 +0400 Subject: [PATCH 19/23] Add new socket symbols to fix nightly build --- trio/socket.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/trio/socket.py b/trio/socket.py index fa51f6dbf1..5951b5b099 100644 --- a/trio/socket.py +++ b/trio/socket.py @@ -107,7 +107,15 @@ CAN_BCM_TX_RESET_MULTI_IDX, IPPROTO_CBT, IPPROTO_ICLFXBM, IPPROTO_IGP, IPPROTO_L2TP, IPPROTO_PGM, IPPROTO_RDP, IPPROTO_ST, AF_QIPCRTR, CAN_BCM_CAN_FD_FRAME, IPPROTO_MOBILE, IPV6_USE_MIN_MTU, - MSG_NOTIFICATION, SO_SETFIB + MSG_NOTIFICATION, SO_SETFIB, CAN_J1939, CAN_RAW_JOIN_FILTERS, + IPPROTO_UDPLITE, J1939_EE_INFO_NONE, J1939_EE_INFO_TX_ABORT, + J1939_FILTER_MAX, J1939_IDLE_ADDR, J1939_MAX_UNICAST_ADDR, + J1939_NLA_BYTES_ACKED, J1939_NLA_PAD, J1939_NO_ADDR, J1939_NO_NAME, + J1939_NO_PGN, J1939_PGN_ADDRESS_CLAIMED, J1939_PGN_ADDRESS_COMMANDED, + J1939_PGN_MAX, J1939_PGN_PDU1_MAX, J1939_PGN_REQUEST, + SCM_J1939_DEST_ADDR, SCM_J1939_DEST_NAME, SCM_J1939_ERRQUEUE, + SCM_J1939_PRIO, SO_J1939_ERRQUEUE, SO_J1939_FILTER, SO_J1939_PROMISC, + SO_J1939_SEND_PRIO, UDPLITE_RECV_CSCOV, UDPLITE_SEND_CSCOV ) except ImportError: pass From 5fda1f5c3520e0838d77f50d921aafcb56050380 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 22 May 2020 06:20:09 +0000 Subject: [PATCH 20/23] Bump six from 1.14.0 to 1.15.0 Bumps [six](https://github.com/benjaminp/six) from 1.14.0 to 1.15.0. - [Release notes](https://github.com/benjaminp/six/releases) - [Changelog](https://github.com/benjaminp/six/blob/master/CHANGES) - [Commits](https://github.com/benjaminp/six/compare/1.14.0...1.15.0) Signed-off-by: dependabot-preview[bot] --- docs-requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs-requirements.txt b/docs-requirements.txt index 340141008e..0811d9121c 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -24,7 +24,7 @@ pygments==2.6.1 # via sphinx pyparsing==2.4.7 # via packaging pytz==2020.1 # via babel requests==2.23.0 # via sphinx -six==1.14.0 # via packaging +six==1.15.0 # via packaging sniffio==1.1.0 # via -r docs-requirements.in snowballstemmer==2.0.0 # via sphinx sortedcontainers==2.1.0 # via -r docs-requirements.in diff --git a/test-requirements.txt b/test-requirements.txt index 25b1d7d3e9..f32a41c163 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -41,7 +41,7 @@ pyopenssl==19.1.0 # via -r test-requirements.in pyparsing==2.4.7 # via packaging pytest-cov==2.8.1 # via -r test-requirements.in pytest==5.4.2 # via -r test-requirements.in, pytest-cov -six==1.14.0 # via astroid, cryptography, packaging, pyopenssl, traitlets +six==1.15.0 # via astroid, cryptography, packaging, pyopenssl, traitlets sniffio==1.1.0 # via -r test-requirements.in sortedcontainers==2.1.0 # via -r test-requirements.in toml==0.10.1 # via pylint From a3bc1f90077f4b80f3893438b447645690262ec2 Mon Sep 17 00:00:00 2001 From: Joshua Oreman Date: Fri, 22 May 2020 00:55:19 -0700 Subject: [PATCH 21/23] Docs: suppress epub warnings for unknown file types (#1541) This looks like it will fix our RTD build. --- docs/source/conf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 49afad6cfd..9dddc93400 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -145,6 +145,10 @@ def setup(app): # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False +# This avoids a warning by the epub builder that it can't figure out +# the MIME type for our favicon. +suppress_warnings = ["epub.unknown_project_files"] + # -- Options for HTML output ---------------------------------------------- From 9f4b571141c15a41860e09570865fd7a041a4e5d Mon Sep 17 00:00:00 2001 From: Joshua Oreman Date: Fri, 22 May 2020 08:03:00 +0000 Subject: [PATCH 22/23] Bump version and run towncrier for 0.15.1 release --- docs/source/history.rst | 12 ++++++++++++ newsfragments/1244.bugfix.rst | 1 - trio/_version.py | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) delete mode 100644 newsfragments/1244.bugfix.rst diff --git a/docs/source/history.rst b/docs/source/history.rst index 133b615d85..d6967217c0 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -5,6 +5,18 @@ Release history .. towncrier release notes start +Trio 0.15.1 (2020-05-22) +------------------------ + +Bugfixes +~~~~~~~~ + +- Fix documentation build. (This must be a new release tag to get readthedocs + "stable" to include the changes from 0.15.0.) + +- Added a helpful error message if an async function is passed to `trio.from_thread.run_sync` or a sync function to `trio.from_thread.run`. (`#1244 `__) + + Trio 0.15.0 (2020-05-19) ------------------------ diff --git a/newsfragments/1244.bugfix.rst b/newsfragments/1244.bugfix.rst deleted file mode 100644 index 6245199a2b..0000000000 --- a/newsfragments/1244.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Added a helpful error message if an async function is passed to `trio.from_thread.run_sync` or a sync function to `trio.from_thread.run`. diff --git a/trio/_version.py b/trio/_version.py index 4a06ac8f47..f137b94817 100644 --- a/trio/_version.py +++ b/trio/_version.py @@ -1,3 +1,3 @@ # This file is imported from __init__.py and exec'd from setup.py -__version__ = "0.15.0+dev" +__version__ = "0.15.1" From 9916aa8fa6aa4013e4117559e48f3377d3318cdd Mon Sep 17 00:00:00 2001 From: Joshua Oreman Date: Fri, 22 May 2020 08:27:26 +0000 Subject: [PATCH 23/23] Bump version to 0.15.1+dev post release --- trio/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trio/_version.py b/trio/_version.py index f137b94817..4d8550b19f 100644 --- a/trio/_version.py +++ b/trio/_version.py @@ -1,3 +1,3 @@ # This file is imported from __init__.py and exec'd from setup.py -__version__ = "0.15.1" +__version__ = "0.15.1+dev"