diff --git a/.appveyor.yml b/.appveyor.yml index b8373d53..c189e4fa 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -4,12 +4,16 @@ os: Visual Studio 2015 environment: matrix: - #- PYTHON: "C:\\Python35" - #- PYTHON: "C:\\Python35-x64" - PYTHON: "C:\\Python36" - PYTHON: "C:\\Python36-x64" - PYTHON: "C:\\Python37" - PYTHON: "C:\\Python37-x64" + # The Windows Python 3.8 tests currently fail on + # test_asyncio.test_windows_events.ProactorLoopCtrlC where `get_event_loop` + # fails to start a new loop + # https://ci.appveyor.com/project/smurfix/trio-asyncio/builds/33592418/job/06yddkw8wy9y6j1h + # - PYTHON: "C:\\Python38" + # - PYTHON: "C:\\Python38-x64" build_script: - "git --no-pager log -n2" @@ -26,5 +30,5 @@ test_script: - "cd empty" # Make sure it's being imported from where we expect - "python -c \"import os, trio_asyncio; print(os.path.dirname(trio_asyncio.__file__))\"" - - "python -u -m pytest -W error -ra -v -s --cov=trio_asyncio --cov-config=../.coveragerc ../tests" + - "python -u -m pytest -ra -v -s --cov=trio_asyncio --cov-config=../.coveragerc ../tests" - "codecov" diff --git a/.travis.yml b/.travis.yml index 5de744dd..e7e192b3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ dist: xenial matrix: include: - - python: 3.5 - python: 3.6 - python: 3.7 - python: 3.7-dev @@ -12,7 +11,13 @@ matrix: - python: nightly - os: osx language: generic - env: MACPYTHON=3.6.3 + env: MACPYTHON=3.6.8 # last binary release + - os: osx + language: generic + env: MACPYTHON=3.7.7 # last binary release + - os: osx + language: generic + env: MACPYTHON=3.8.3 - python: 3.6 env: CHECK_DOCS=1 - python: 3.6 diff --git a/README.rst b/README.rst index 1330fd17..f77e91f3 100644 --- a/README.rst +++ b/README.rst @@ -26,8 +26,8 @@ **trio-asyncio** is a re-implementation of the ``asyncio`` mainloop on top of Trio. -trio-asyncio requires at least Python 3.5.3. It is tested on recent versions of -3.5, 3.6, 3.7, 3.8, and nightly. +Trio-Asyncio requires at least Python 3.6. It is tested on recent versions of +3.6, 3.7, 3.8, and nightly. +++++++++++ Rationale diff --git a/ci/travis.sh b/ci/travis.sh index 7fd4cabf..dc1aae28 100755 --- a/ci/travis.sh +++ b/ci/travis.sh @@ -6,7 +6,7 @@ set -ex YAPF_VERSION=0.20.0 if [ "$TRAVIS_OS_NAME" = "osx" ]; then - curl -Lo macpython.pkg https://www.python.org/ftp/python/${MACPYTHON}/python-${MACPYTHON}-macosx10.6.pkg + curl -Lo macpython.pkg https://www.python.org/ftp/python/${MACPYTHON}/python-${MACPYTHON}-macosx10.9.pkg sudo installer -pkg macpython.pkg -target / ls /Library/Frameworks/Python.framework/Versions/*/bin/ PYTHON_EXE=/Library/Frameworks/Python.framework/Versions/*/bin/python3 diff --git a/docs/source/index.rst b/docs/source/index.rst index 1be6f2ab..13f96882 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -51,7 +51,7 @@ asyncio libraries such as ``home-assistant``. Helpful facts: * Supported environments: Linux, MacOS, or Windows running some kind of Python - 3.5.3-or-better (either CPython or PyPy3 is fine). \*BSD and illumOS likely + 3.6-or-better (either CPython or PyPy3 is fine). \*BSD and illumOS likely work too, but are untested. * Install: ``python3 -m pip install -U trio-asyncio`` (or on Windows, maybe diff --git a/docs/source/principles.rst b/docs/source/principles.rst index fc2c2b79..90944353 100644 --- a/docs/source/principles.rst +++ b/docs/source/principles.rst @@ -81,7 +81,7 @@ Most *synchronous* asyncio or Trio functions (:meth:`trio.Event.set`, asyncio or Trio context, and work equally well regardless of the flavor of function calling them. The exceptions are functions that access the current task (:func:`asyncio.current_task`, -:func:`trio.hazmat.current_task`, and anything that calls them), +:func:`trio.lowlevel.current_task`, and anything that calls them), because there's only a meaningful concept of the current *foo* task when a *foo*-flavored function is executing. For example, this means context managers that set a timeout on their body (``with diff --git a/newsfragments/82.misc.rst b/newsfragments/82.misc.rst new file mode 100644 index 00000000..bc9a04dc --- /dev/null +++ b/newsfragments/82.misc.rst @@ -0,0 +1,3 @@ +``trio-asyncio`` now requires Trio 0.15. + +Support for Python < 3.6 has been removed. diff --git a/setup.cfg b/setup.cfg index bf951416..7dcf898b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,3 +5,6 @@ max-line-length=99 ignore=E402,E731,E127,E502,E123,W503 [tool:pytest] addopts = -p no:asyncio +filterwarnings = + error + ignore:The loop argument is deprecated*:DeprecationWarning diff --git a/setup.py b/setup.py index f0d7cb4d..026d5442 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ code (asyncio: ~8000) but passes the complete Python 3.6 test suite with no errors. -``trio_asyncio`` requires Python 3.5.3 or better. +``trio_asyncio`` requires Python 3.6 or better. Author ====== @@ -53,12 +53,12 @@ """ install_requires = [ - "trio >= 0.12.0", - "async_generator >= 1.6", + "trio >= 0.15.0", "outcome", ] if sys.version_info < (3, 7): install_requires.append("contextvars >= 2.1") + install_requires.append("async_generator >= 1.6") setup( name="trio_asyncio", @@ -74,10 +74,10 @@ # This means, just install *everything* you see under trio/, even if it # doesn't look like a source file, so long as it appears in MANIFEST.in: include_package_data=True, - python_requires=">=3.5.2", # temporary, for RTD + python_requires=">=3.6", # temporary, for RTD keywords=["async", "io", "trio", "asyncio", "trio-asyncio"], setup_requires=['pytest-runner'], - tests_require=['pytest', 'outcome'], + tests_require=['pytest >= 5.4', 'pytest-trio >= 0.6', 'outcome'], classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", diff --git a/tests/aiotest/test_callback.py b/tests/aiotest/test_callback.py index 5c0ee6fa..d9419fa6 100644 --- a/tests/aiotest/test_callback.py +++ b/tests/aiotest/test_callback.py @@ -45,8 +45,7 @@ async def test_close(self, loop, config): await loop.stop().wait() loop.close() - @config.asyncio.coroutine - def test(): + async def test(): pass func = lambda: False diff --git a/tests/conftest.py b/tests/conftest.py index 0e320828..0c3e77be 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,6 @@ import asyncio import trio_asyncio import inspect -from async_generator import async_generator, yield_ # Hacks for <3.7 if not hasattr(asyncio, 'run'): @@ -45,11 +44,10 @@ def create_task(coro): @pytest.fixture -@async_generator async def loop(): async with trio_asyncio.open_loop() as loop: try: - await yield_(loop) + yield loop finally: await loop.stop().wait() diff --git a/tests/interop/test_adapter.py b/tests/interop/test_adapter.py index 09e54fdf..0c8bf48b 100644 --- a/tests/interop/test_adapter.py +++ b/tests/interop/test_adapter.py @@ -1,5 +1,4 @@ import pytest -from async_generator import async_generator, yield_ from trio_asyncio import aio_as_trio, trio_as_aio, allow_asyncio from trio_asyncio import aio2trio, trio2aio import asyncio @@ -8,7 +7,10 @@ from tests import aiotest import sys import warnings -from async_generator import asynccontextmanager +try: + from contextlib import asynccontextmanager +except ImportError: + from async_generator import asynccontextmanager from .. import utils as test_utils from trio_asyncio import TrioAsyncioDeprecationWarning @@ -74,43 +76,39 @@ async def dly_asyncio(self, do_test=True): self.flag |= 1 return 4 - @async_generator async def iter_asyncio(self, do_test=True): if do_test and sys.version_info >= (3, 7): assert sniffio.current_async_library() == "asyncio" await asyncio.sleep(0.01, loop=self.loop) - await yield_(1) + yield 1 await asyncio.sleep(0.01, loop=self.loop) - await yield_(2) + yield 2 await asyncio.sleep(0.01, loop=self.loop) self.flag |= 1 - @async_generator async def iter_trio(self): if sys.version_info >= (3, 7): assert sniffio.current_async_library() == "trio" await trio.sleep(0.01) - await yield_(1) + yield 1 await trio.sleep(0.01) - await yield_(2) + yield 2 await trio.sleep(0.01) self.flag |= 1 @asynccontextmanager - @async_generator async def ctx_asyncio(self): await asyncio.sleep(0.01, loop=self.loop) self.flag |= 1 - await yield_(self) + yield self await asyncio.sleep(0.01, loop=self.loop) self.flag |= 2 @asynccontextmanager - @async_generator async def ctx_trio(self): await trio.sleep(0.01) self.flag |= 1 - await yield_(self) + yield self await trio.sleep(0.01) self.flag |= 2 diff --git a/tests/interop/test_calls.py b/tests/interop/test_calls.py index a5d59cf7..ef8dbfac 100644 --- a/tests/interop/test_calls.py +++ b/tests/interop/test_calls.py @@ -2,7 +2,6 @@ import asyncio import trio import sniffio -from async_generator import async_generator, yield_ from trio_asyncio import aio_as_trio, trio_as_aio from tests import aiotest from functools import partial @@ -303,7 +302,7 @@ async def test_asyncio_trio_cancel_out(self, loop): async def cancelled_trio(seen): seen.flag |= 1 await trio.sleep(0.01) - scope = trio.hazmat.current_task()._cancel_status._scope + scope = trio.lowlevel.current_task()._cancel_status._scope scope.cancel() seen.flag |= 2 await trio.sleep(0.01) @@ -526,11 +525,10 @@ def err_asyncio(): @pytest.mark.trio async def test_trio_asyncio_generator(self, loop): - @async_generator async def dly_asyncio(): - await yield_(1) + yield 1 await asyncio.sleep(0.01, loop=loop) - await yield_(2) + yield 2 with test_utils.deprecate(self): res = await async_gen_to_list(loop.wrap_generator(dly_asyncio)) @@ -538,11 +536,10 @@ async def dly_asyncio(): @pytest.mark.trio async def test_trio_asyncio_generator_with_error(self, loop): - @async_generator async def dly_asyncio(): - await yield_(1) + yield 1 raise RuntimeError("I has an owie") - await yield_(2) + yield 2 with test_utils.deprecate(self): with pytest.raises(RuntimeError) as err: @@ -551,9 +548,8 @@ async def dly_asyncio(): @pytest.mark.trio async def test_trio_asyncio_generator_with_cancellation(self, loop): - @async_generator async def dly_asyncio(hold, seen): - await yield_(1) + yield 1 seen.flag |= 1 await hold.wait() @@ -573,11 +569,10 @@ async def cancel_soon(nursery): @pytest.mark.trio async def test_trio_asyncio_iterator(self, loop): - @async_generator async def slow_nums(): for n in range(1, 6): await asyncio.sleep(0.01, loop=loop) - await yield_(n) + yield n sum = 0 async for n in aio_as_trio(slow_nums()): @@ -586,11 +581,10 @@ async def slow_nums(): @pytest.mark.trio async def test_trio_asyncio_iterator_depr(self, loop): - @async_generator async def slow_nums(): for n in range(1, 6): await asyncio.sleep(0.01, loop=loop) - await yield_(n) + yield n sum = 0 # with test_utils.deprecate(self): ## not yet diff --git a/tests/python/conftest.py b/tests/python/conftest.py index 6fee94a1..74c11cca 100644 --- a/tests/python/conftest.py +++ b/tests/python/conftest.py @@ -21,7 +21,11 @@ # to the event loop in the testsuite class, because # SyncTrioEventLoop spawns a thread that only exits when the # loop is closed. Nerf it. - from test.support import threading_cleanup + try: + from test.support import threading_cleanup + except ImportError: + # Python 3.10+ + from test.support.threading_helper import threading_cleanup def threading_no_cleanup(*original_values): pass @@ -124,7 +128,7 @@ def skip(rel_id): # These fail with ConnectionResetError on Pythons <= 3.7.x # for some unknown x. (3.7.1 fails, 3.7.5 and 3.7.6 pass; - # older 3.6.x also affected, and older-or-all 3.5.x) + # older 3.6.x also affected) if sys.platform != "win32" and sys.version_info < (3, 8): import selectors diff --git a/tests/test_misc.py b/tests/test_misc.py index 31cdc9e4..7ff0c6c3 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -30,7 +30,7 @@ def close_no_stop(): async def test_too_many_stops(self): with trio.move_on_after(1) as scope: async with trio_asyncio.open_loop() as loop: - await trio.hazmat.checkpoint() + await trio.lowlevel.checkpoint() loop.stop() assert not scope.cancelled_caught, \ "Possible deadlock after manual call to loop.stop" @@ -167,7 +167,7 @@ async def nest(x): with pytest.raises(RuntimeError): trio_asyncio.run_trio_task(nest, 100) - with pytest.raises(RuntimeError): + with pytest.raises((AttributeError, RuntimeError)): with trio_asyncio.open_loop(): nest(1000) diff --git a/tests/test_trio_asyncio.py b/tests/test_trio_asyncio.py new file mode 100644 index 00000000..c16b2f2e --- /dev/null +++ b/tests/test_trio_asyncio.py @@ -0,0 +1,38 @@ +import pytest +import sys +import asyncio +from async_generator import async_generator, yield_ +import trio_asyncio + + +async def use_asyncio(): + await trio_asyncio.aio_as_trio(asyncio.sleep)(0) + + +@pytest.fixture() +async def asyncio_fixture_with_fixtured_loop(loop): + await use_asyncio() + yield None + + +@pytest.fixture() +async def asyncio_fixture_own_loop(): + async with trio_asyncio.open_loop(): + await use_asyncio() + yield None + + +@pytest.mark.trio +async def test_no_fixture(): + async with trio_asyncio.open_loop(): + await use_asyncio() + + +@pytest.mark.trio +async def test_half_fixtured_asyncpg_conn(asyncio_fixture_own_loop): + await use_asyncio() + + +@pytest.mark.trio +async def test_fixtured_asyncpg_conn(asyncio_fixture_with_fixtured_loop): + await use_asyncio() diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..c9471946 --- /dev/null +++ b/tox.ini @@ -0,0 +1,44 @@ +[tox] +envlist = py36,py37 + +[sphinx-vars] +build_dir = build +input_dir = docs/source +sphinxopts = -a -W -b html +autosphinxopts = -i *~ -i *.sw* -i Makefile* +sphinxbuilddir = {[sphinx-vars]build_dir}/sphinx/html +allsphinxopts = -d {[sphinx-vars]build_dir}/sphinx/doctrees {[sphinx-vars]sphinxopts} docs + +[testenv] +deps = + pytest + outcome +commands = + pytest -xvvv --full-trace + +[testenv:pylint] +deps = + pylint +commands = + pylint trio_asyncio + +[testenv:flake8] +deps = + flake8 +commands = + flake8 trio_asyncio tests + +[testenv:doc] +deps = + sphinx + sphinx-rtd-theme + sphinxcontrib-trio +commands = + sphinx-build -a {[sphinx-vars]input_dir} {[sphinx-vars]build_dir} + +[testenv:livehtml] +deps = + sphinx + sphinx-autobuild +commands = + sphinx-autobuild {[sphinx-vars]autosphinxopts} {[sphinx-vars]allsphinxopts} {[sphinx-vars]sphinxbuilddir} diff --git a/trio_asyncio/_base.py b/trio_asyncio/_base.py index 78aadedf..4329d1f8 100644 --- a/trio_asyncio/_base.py +++ b/trio_asyncio/_base.py @@ -17,7 +17,7 @@ from selectors import _BaseSelectorImpl, EVENT_READ, EVENT_WRITE try: - from trio.hazmat import wait_for_child + from trio.lowlevel import wait_for_child except ImportError: from ._child import wait_for_child @@ -27,11 +27,11 @@ _mswindows = (sys.platform == "win32") try: - _wait_readable = trio.hazmat.wait_readable - _wait_writable = trio.hazmat.wait_writable + _wait_readable = trio.lowlevel.wait_readable + _wait_writable = trio.lowlevel.wait_writable except AttributeError: - _wait_readable = trio.hazmat.wait_socket_readable - _wait_writable = trio.hazmat.wait_socket_writable + _wait_readable = trio.lowlevel.wait_socket_readable + _wait_writable = trio.lowlevel.wait_socket_writable class _Clear: @@ -466,7 +466,7 @@ def add_reader(self, fd, callback, *args): ready for reading. This creates a new Trio task. You may want to use "await - :obj:`trio.hazmat.wait_readable` (fd)" instead, or + :obj:`trio.lowlevel.wait_readable` (fd)" instead, or :param fd: Either an integer (Unix file descriptor) or an object with a ``fileno`` method providing one. @@ -516,7 +516,7 @@ def add_writer(self, fd, callback, *args): ready for writing. This creates a new Trio task. You may want to use "await - :obj:`trio.hazmat.wait_writable` (fd) instead, or + :obj:`trio.lowlevel.wait_writable` (fd) instead, or :param fd: Either an integer (Unix file descriptor) or an object with a ``fileno`` method providing one. @@ -614,8 +614,8 @@ async def _main_loop_init(self, nursery): if self._nursery is not None or not self._stopped.is_set(): raise RuntimeError("You can't enter a loop twice") self._nursery = nursery - self._task = trio.hazmat.current_task() - self._token = trio.hazmat.current_trio_token() + self._task = trio.lowlevel.current_task() + self._token = trio.lowlevel.current_trio_token() async def _main_loop(self, task_status=trio.TASK_STATUS_IGNORED): """Run the loop by processing its event queue. diff --git a/trio_asyncio/_child.py b/trio_asyncio/_child.py index 12a6a4b4..073d31e2 100644 --- a/trio_asyncio/_child.py +++ b/trio_asyncio/_child.py @@ -96,7 +96,7 @@ async def wait(self): async def _start_waiting(self): """Start the background thread that waits for a specific child""" self.__event = trio.Event() - self.__token = trio.hazmat.current_trio_token() + self.__token = trio.lowlevel.current_trio_token() self.__thread = threading.Thread( target=self._wait_thread, name="waitpid_%d" % self.__pid, daemon=True diff --git a/trio_asyncio/_loop.py b/trio_asyncio/_loop.py index 1660000e..b47d208d 100644 --- a/trio_asyncio/_loop.py +++ b/trio_asyncio/_loop.py @@ -8,14 +8,17 @@ import threading from contextvars import ContextVar -from async_generator import async_generator, yield_, asynccontextmanager +try: + from contextlib import asynccontextmanager +except ImportError: + from async_generator import asynccontextmanager from ._util import run_aio_future, run_aio_generator from ._async import TrioEventLoop from ._deprecate import deprecated, warn_deprecated try: - from trio.hazmat import wait_for_child + from trio.lowlevel import wait_for_child except ImportError: from ._child import wait_for_child @@ -100,7 +103,7 @@ class _FakedPolicy(threading.local): def _in_trio_context(): try: - trio.hazmat.current_task() + trio.lowlevel.current_task() except RuntimeError: return False else: @@ -144,7 +147,7 @@ def get_event_loop(self): ``.current_event_loop`` property. """ try: - task = trio.hazmat.current_task() + task = trio.lowlevel.current_task() except RuntimeError: pass else: @@ -227,7 +230,7 @@ def _new_policy_set(new_policy): def _new_run_get(): try: - task = trio.hazmat.current_task() + task = trio.lowlevel.current_task() except RuntimeError: pass else: @@ -368,7 +371,6 @@ def __exit__(self, *tb): @asynccontextmanager -@async_generator async def open_loop(queue_len=None): """Returns a Trio-flavored async context manager which provides an asyncio event loop running on top of Trio. @@ -396,7 +398,7 @@ async def async_main(*args): loop._closed = False await loop._main_loop_init(nursery) await nursery.start(loop._main_loop) - await yield_(loop) + yield loop finally: try: await loop._main_loop_exit() diff --git a/trio_asyncio/_util.py b/trio_asyncio/_util.py index 5b5c698a..cfadf56b 100644 --- a/trio_asyncio/_util.py +++ b/trio_asyncio/_util.py @@ -6,7 +6,6 @@ import sys import outcome import sniffio -from async_generator import async_generator, yield_ async def run_aio_future(future): @@ -24,11 +23,11 @@ async def run_aio_future(future): This is a Trio-flavored async function. """ - task = trio.hazmat.current_task() + task = trio.lowlevel.current_task() raise_cancel = None def done_cb(_): - trio.hazmat.reschedule(task, outcome.capture(future.result)) + trio.lowlevel.reschedule(task, outcome.capture(future.result)) future.add_done_callback(done_cb) @@ -39,10 +38,10 @@ def abort_cb(raise_cancel_arg): # Attempt to cancel our future future.cancel() # Keep waiting - return trio.hazmat.Abort.FAILED + return trio.lowlevel.Abort.FAILED try: - res = await trio.hazmat.wait_task_rescheduled(abort_cb) + res = await trio.lowlevel.wait_task_rescheduled(abort_cb) return res except asyncio.CancelledError as exc: if raise_cancel is not None: @@ -59,14 +58,13 @@ def abort_cb(raise_cancel_arg): STOP = object() -@async_generator async def run_aio_generator(loop, async_generator): """Return a Trio-flavored async iterator which wraps the given asyncio-flavored async iterator (usually an async generator, but doesn't have to be). The asyncio tasks that perform each iteration of *async_generator* will run in *loop*. """ - task = trio.hazmat.current_task() + task = trio.lowlevel.current_task() raise_cancel = None current_read = None @@ -85,7 +83,7 @@ async def consume_next(): finally: sniffio.current_async_library_cvar.reset(t) - trio.hazmat.reschedule(task, result) + trio.lowlevel.reschedule(task, result) def abort_cb(raise_cancel_arg): # Save the cancel-raising function @@ -94,29 +92,29 @@ def abort_cb(raise_cancel_arg): if not current_read: # There is no current read - return trio.hazmat.Abort.SUCCEEDED + return trio.lowlevel.Abort.SUCCEEDED else: # Attempt to cancel the current iterator read, do not # report success until the future was actually cancelled. already_cancelled = current_read.cancel() if already_cancelled: - return trio.hazmat.Abort.SUCCEEDED + return trio.lowlevel.Abort.SUCCEEDED else: # Continue dealing with the cancellation once # future.cancel() goes to the result of # wait_task_rescheduled() - return trio.hazmat.Abort.FAILED + return trio.lowlevel.Abort.FAILED try: while True: # Schedule in asyncio that we read the next item from the iterator current_read = asyncio.ensure_future(consume_next(), loop=loop) - item = await trio.hazmat.wait_task_rescheduled(abort_cb) + item = await trio.lowlevel.wait_task_rescheduled(abort_cb) if item is STOP: break - await yield_(item) + yield item except asyncio.CancelledError as exc: if raise_cancel is not None: @@ -130,7 +128,6 @@ def abort_cb(raise_cancel_arg): raise -@async_generator async def run_trio_generator(loop, async_generator): """Run a Trio generator from within asyncio""" while True: @@ -140,7 +137,7 @@ async def run_trio_generator(loop, async_generator): except StopAsyncIteration: break else: - await yield_(item) + yield item # Copied from Trio: