Hello community, here is the log from the commit of package python-curio for openSUSE:Factory checked in at 2020-10-26 16:08:54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-curio (Old) and /work/SRC/openSUSE:Factory/.python-curio.new.3463 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-curio" Mon Oct 26 16:08:54 2020 rev:6 rq:843295 version:1.4 Changes: -------- --- /work/SRC/openSUSE:Factory/python-curio/python-curio.changes 2020-06-15 20:33:20.506976803 +0200 +++ /work/SRC/openSUSE:Factory/.python-curio.new.3463/python-curio.changes 2020-10-26 16:09:11.218543595 +0100 @@ -1,0 +2,9 @@ +Thu Oct 22 03:11:22 UTC 2020 - Steve Kowalik <steven.kowa...@suse.com> + +- Update to 1.4: + * Fixed minimum requirement in setup.py + * Moved the Pytest plugin to the examples directory. + * Refined the detection of coroutines to use collections.abc.Coroutine. + * Added a Result object. + +------------------------------------------------------------------- Old: ---- curio-1.2.tar.gz New: ---- curio-1.4.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-curio.spec ++++++ --- /var/tmp/diff_new_pack.rJy6RW/_old 2020-10-26 16:09:12.954545193 +0100 +++ /var/tmp/diff_new_pack.rJy6RW/_new 2020-10-26 16:09:12.958545197 +0100 @@ -19,7 +19,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %define skip_python2 1 Name: python-curio -Version: 1.2 +Version: 1.4 Release: 0 Summary: Concurrent I/O library for Python 3 License: BSD-Source-Code ++++++ curio-1.2.tar.gz -> curio-1.4.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curio-1.2/CHANGES new/curio-1.4/CHANGES --- old/curio-1.2/CHANGES 2020-04-07 03:43:23.000000000 +0200 +++ new/curio-1.4/CHANGES 2020-08-24 11:41:46.000000000 +0200 @@ -1,6 +1,67 @@ CHANGES ------- -Version 1.2 - In Progress +Version 1.4 - August 23, 2020 +----------------------------- +08/23/2020 Fixed minimum requirement in setup.py + +Version 1.3 - August 23, 2020 +----------------------------- +08/21/2020 Moved the Pytest plugin to the examples directory. There have + been several reported problems with it. It is no longer + installed by default. It was never used by Curio itself. + +06/16/2020 Refined the detection of coroutines to use collections.abc.Coroutine. + This change should not affect any existing part of Curio, but it + allows it to properly recognize async functions defined in + extensions such as Cython. See Issue #326. + +06/11/2020 Added a Result object. It's like an Event except that it has a + an associated value/exception attached to it. Here's the basic + usage pattern: + + result = Result() + ... + async def do_work(): + try: + ... + await result.set_value(value) + except Exception as e: + await result.set_exception(e) + + async def some_task(): + ... + try: + value = await result.unwrap() + print("Success:", value) + except Exception as e: + print("Fail:", e) + + In this example, the unwrap() method blocks until the result + becomes available. + +06/09/2020 Having now needed it a few projects, have added a UniversalResult + object. It allows Curio tasks or threads to wait for a result + to be set by another thread or task. For example: + + def do_work(result): + ... + result.set_value(value) + + async def some_task(result): + ... + value = await result.unwrap() + ... + + result = UniversalResult() + threading.Thread(target=do_work, args=[result]).start() + curio.run(some_task, result) + + UniversalResult is somewhat similar to a Future. However, it + really only allows setting and waiting. There are no callbacks, + cancellation, or any other extras. + +Version 1.2 - April 6, 2020 +--------------------------- 04/06/2020 Removed hard dependency on contextvars. It was unnecessary and needlessly broken some things on Python 3.6. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curio-1.2/curio/__init__.py new/curio-1.4/curio/__init__.py --- old/curio-1.2/curio/__init__.py 2020-04-07 03:43:23.000000000 +0200 +++ new/curio-1.4/curio/__init__.py 2020-08-24 11:41:46.000000000 +0200 @@ -1,6 +1,6 @@ # curio/__init__.py -__version__ = '1.2' +__version__ = '1.4' from .errors import * from .queue import * diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curio-1.2/curio/__main__.py new/curio-1.4/curio/__main__.py --- old/curio-1.2/curio/__main__.py 2020-04-07 03:43:23.000000000 +0200 +++ new/curio-1.4/curio/__main__.py 2020-08-24 11:41:46.000000000 +0200 @@ -9,6 +9,8 @@ import signal import os +assert (sys.version_info.major >= 3 and sys.version_info.minor >= 8), "console requires Python 3.8+" + class CurioIOInteractiveConsole(code.InteractiveConsole): def __init__(self, locals): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curio-1.2/curio/meta.py new/curio-1.4/curio/meta.py --- old/curio-1.2/curio/meta.py 2020-04-07 03:43:23.000000000 +0200 +++ new/curio-1.4/curio/meta.py 2020-08-24 11:41:46.000000000 +0200 @@ -20,6 +20,7 @@ import asyncio import threading from contextlib import contextmanager +import collections.abc # -- Curio @@ -94,13 +95,13 @@ it's not a coroutine, we call corofunc(*args, **kwargs) and hope for the best. ''' - if inspect.iscoroutine(corofunc) or inspect.isgenerator(corofunc): + if isinstance(corofunc, collections.abc.Coroutine) or inspect.isgenerator(corofunc): assert not args and not kwargs, "arguments can't be passed to an already instantiated coroutine" return corofunc if not iscoroutinefunction(corofunc) and not getattr(corofunc, '_async_thread', False): coro = corofunc(*args, **kwargs) - if not inspect.iscoroutine(coro): + if not isinstance(coro, collections.abc.Coroutine): raise TypeError(f'Could not create coroutine from {corofunc}') return coro diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curio-1.2/curio/pytest_plugin.py new/curio-1.4/curio/pytest_plugin.py --- old/curio-1.2/curio/pytest_plugin.py 2020-04-07 03:43:23.000000000 +0200 +++ new/curio-1.4/curio/pytest_plugin.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,91 +0,0 @@ -# python3.7 - -"""Plugin module for pytest. - -This enables easier unit tests for applications that use both Curio and Pytest. If you have Curio -installed, you have the plugin and can write unit tests per the example below. - -Provides a fixture named `kernel`, and a marker (pytest.mark.curio) that will run a bare coroutine -in a new Kernel instance. - -Example: - - from curio import sleep - import pytest - - # Use marker - - @pytest.mark.curio - async def test_coro(): - await sleep(1) - - - # Use kernel fixture - - def test_app(kernel): - - async def my_aapp(): - await sleep(1) - - kernel.run(my_aapp) -""" - -import inspect -import functools - -import pytest - -from curio import Kernel -from curio import meta -from curio import monitor -from curio.debug import longblock, logcrash - - -def _is_coroutine(obj): - """Check to see if an object is really a coroutine.""" - return meta.iscoroutinefunction(obj) or inspect.isgeneratorfunction(obj) - - -def pytest_configure(config): - """Inject documentation.""" - config.addinivalue_line("markers", - "curio: " - "mark the test as a coroutine, it will be run using a Curio kernel.") - - -@pytest.mark.tryfirst -def pytest_pycollect_makeitem(collector, name, obj): - """A pytest hook to collect coroutines in a test module.""" - if collector.funcnamefilter(name) and _is_coroutine(obj): - item = pytest.Function(name, parent=collector) - if 'curio' in item.keywords: - return list(collector._genfunctions(name, obj)) - - -@pytest.hookimpl(tryfirst=True, hookwrapper=True) -def pytest_pyfunc_call(pyfuncitem): - """Run curio marked test functions in a Curio kernel instead of a normal function call. - """ - if 'curio' in pyfuncitem.keywords: - pyfuncitem.obj = wrap_in_sync(pyfuncitem.obj) - yield - - -def wrap_in_sync(func): - """Return a sync wrapper around an async function executing it in a Kernel.""" - @functools.wraps(func) - def inner(**kwargs): - coro = func(**kwargs) - Kernel().run(coro, shutdown=True) - return inner - - -# Fixture for explicitly running in Kernel instance. -@pytest.fixture(scope='session') -def kernel(request): - """Provide a Curio Kernel object for running co-routines.""" - k = Kernel(debug=[longblock, logcrash]) - m = monitor.Monitor(k) - request.addfinalizer(lambda: k.run(shutdown=True)) - request.addfinalizer(m.close) - return k diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curio-1.2/curio/sync.py new/curio-1.4/curio/sync.py --- old/curio-1.2/curio/sync.py 2020-04-07 03:43:23.000000000 +0200 +++ new/curio-1.4/curio/sync.py 2020-08-24 11:41:46.000000000 +0200 @@ -10,7 +10,7 @@ # a queue. When a task releases a lock, it wakes a sleeping task. # Task scheduling is provided by the SchedFIFO and SchedBarrier classes in sched.py -__all__ = ['Event', 'UniversalEvent', 'Lock', 'RLock', 'Semaphore', 'Condition' ] +__all__ = ['Event', 'UniversalEvent', 'Lock', 'RLock', 'Semaphore', 'Condition', 'Result', 'UniversalResult' ] # -- Standard library @@ -260,6 +260,88 @@ async def notify_all(self): await self.notify(len(self._waiting)) +class Result: + def __init__(self): + self._evt = Event() + self._value = None + self._exc = None + + def __repr__(self): + res = super().__repr__() + if self._evt.is_set(): + return f'<{res[1:-1]}, value={self._value!r}, exc={self._exc!r}>' + else: + return f'<{res[1:-1]}, not set>' + + status = "set" if self.is_set() else "not set" + return f'<Result status={status}>' + + def is_set(self): + return self._evt.is_set() + + async def unwrap(self): + await self._evt.wait() + if self._exc: + raise self._exc from None + else: + return self._value + + async def set_value(self, value): + self._value = value + await self._evt.set() + + async def set_exception(self, exc): + self._exc = exc + await self._evt.set() + +class UniversalResult: + + def __init__(self): + self._evt = UniversalEvent() + self._value = None + self._exc = None + + def __repr__(self): + res = super().__repr__() + if self._evt.is_set(): + return f'<{res[1:-1]}, value={self._value!r}, exc={self._exc!r}>' + else: + return f'<{res[1:-1]}, not set>' + + def is_set(self): + return self._evt.is_set() + + def _return_result(self): + if self._exc: + raise self._exc from None + else: + return self._value + + def unwrap(self): + self._evt.wait() + return self._return_result() + + @awaitable(unwrap) + async def unwrap(self): + await self._evt.wait() + return self._return_result() + + def set_value(self, value): + self._value = value + self._evt.set() + + @awaitable(set_value) + async def set_value(self, value): + self._value = value + await self._evt.set() + def set_exception(self, exc): + self._exc = exc + self._evt.set() + + @awaitable(set_exception) + async def set_exception(self, exc): + self._exc = exc + await self._evt.set() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curio-1.2/docs/conf.py new/curio-1.4/docs/conf.py --- old/curio-1.2/docs/conf.py 2020-04-07 03:43:23.000000000 +0200 +++ new/curio-1.4/docs/conf.py 2020-08-24 11:41:46.000000000 +0200 @@ -60,9 +60,9 @@ # built documents. # # The short X.Y version. -version = '1.2' +version = '1.4' # The full version, including alpha/beta/rc tags. -release = '1.2' +release = '1.4' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curio-1.2/docs/reference.rst new/curio-1.4/docs/reference.rst --- old/curio-1.2/docs/reference.rst 2020-04-07 03:43:23.000000000 +0200 +++ new/curio-1.4/docs/reference.rst 2020-08-24 11:41:46.000000000 +0200 @@ -142,7 +142,7 @@ * - ``t.cycles`` - The number of scheduling cycles the task has completed. * - ``t.result`` - - A property holding the task result. If accessed before the a terminates, + - A property holding the task result. If accessed before the task terminates, a ``RuntimeError`` exception is raised. If a task crashed with an exception, that exception is reraised on access. * - ``t.exception`` @@ -458,6 +458,27 @@ * - ``await e.set()`` - Set the event. Wake all waiting tasks (if any) + +.. class:: Result() + + A synchronized result object. This is like an `Event` except + that it additionally carries a value or exception. + +An :class:`Result` instance ``r`` supports the following methods: + +.. list-table:: + :widths: 40 60 + :header-rows: 0 + + * - ``r.is_set()`` + - Return ``True`` if a result value or exception has been set + * - ``await r.set_value(value)`` + - Set the result value, waking any waiters. + * - ``await r.set_exception(exc)`` + - Set the result exception, waking any waiters. + * - ``await r.unwrap()`` + - Wait and return the set value. If an exception was set, it is reraised. + ``Lock``, ``RLock``, ``Semaphore`` classes that allow for mutual exclusion and inter-task coordination. @@ -667,6 +688,19 @@ to coordinate Curio and ``asyncio``, they must be executing in separate threads. +.. class:: UniversalResult() + + A result object that can be used from Curio tasks, threads, and + ``asyncio``. A result is somewhat similar to an event, but it + additionally carries an attached value or exception. To set the + result, use ``set_value()`` or ``set_exception()``. To return the + result, blocking if necessary, use ``unwrap()``. If used in an + asynchronous environment, these operations must be prefaced by + ``await``. If used to coordinate Curio and ``asyncio``, they must + be executing in separate threads. A ``UniversalResult()`` is + somewhat similar to a ``Future`` in usage, but it has a much more + restrictive API. + Here is an example of a producer-consumer problem with a ``UniversalQueue`` involving Curio, threads, and ``asyncio`` all running at once:: @@ -720,8 +754,29 @@ In this code, the ``consumer()`` coroutine is used simultaneously in Curio and ``asyncio``. ``producer()`` is an ordinary thread. -When in doubt, queues and events are the preferred mechanism of -coordinating Curio with foreign environments. Higher-level +Here is example that has Curio wait for the result produced +by a thread using a ``UniversalResult`` object:: + + import threading + import curio + import time + + def worker(x, y, result): + time.sleep(10) + try: + result.set_value(x+y) + except Exception as err: + result.set_exception(err) + + async def main(): + result = curio.UniversalResult() + threading.Thread(target=worker, args=[2,3,result]).start() + print("Result:", await result.unwrap()) + + curio.run(main) + +When in doubt, queues, events, and results are the preferred mechanism +of coordinating Curio with foreign environments. Higher-level abstractions can often be built from these. Blocking Operations and External Work diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curio-1.2/examples/pytest_plugin.py new/curio-1.4/examples/pytest_plugin.py --- old/curio-1.2/examples/pytest_plugin.py 1970-01-01 01:00:00.000000000 +0100 +++ new/curio-1.4/examples/pytest_plugin.py 2020-08-24 11:41:46.000000000 +0200 @@ -0,0 +1,91 @@ +# python3.7 + +"""Plugin module for pytest. + +This enables easier unit tests for applications that use both Curio and Pytest. If you have Curio +installed, you have the plugin and can write unit tests per the example below. + +Provides a fixture named `kernel`, and a marker (pytest.mark.curio) that will run a bare coroutine +in a new Kernel instance. + +Example: + + from curio import sleep + import pytest + + # Use marker + + @pytest.mark.curio + async def test_coro(): + await sleep(1) + + + # Use kernel fixture + + def test_app(kernel): + + async def my_aapp(): + await sleep(1) + + kernel.run(my_aapp) +""" + +import inspect +import functools + +import pytest + +from curio import Kernel +from curio import meta +from curio import monitor +from curio.debug import longblock, logcrash + + +def _is_coroutine(obj): + """Check to see if an object is really a coroutine.""" + return meta.iscoroutinefunction(obj) or inspect.isgeneratorfunction(obj) + + +def pytest_configure(config): + """Inject documentation.""" + config.addinivalue_line("markers", + "curio: " + "mark the test as a coroutine, it will be run using a Curio kernel.") + + +@pytest.mark.tryfirst +def pytest_pycollect_makeitem(collector, name, obj): + """A pytest hook to collect coroutines in a test module.""" + if collector.funcnamefilter(name) and _is_coroutine(obj): + item = pytest.Function.from_parent(collector, name=name) + if 'curio' in item.keywords: + return list(collector._genfunctions(name, obj)) + + +@pytest.hookimpl(tryfirst=True, hookwrapper=True) +def pytest_pyfunc_call(pyfuncitem): + """Run curio marked test functions in a Curio kernel instead of a normal function call. + """ + if pyfuncitem.get_closest_marker('curio'): + pyfuncitem.obj = wrap_in_sync(pyfuncitem.obj) + yield + + +def wrap_in_sync(func): + """Return a sync wrapper around an async function executing it in a Kernel.""" + @functools.wraps(func) + def inner(**kwargs): + coro = func(**kwargs) + Kernel().run(coro, shutdown=True) + return inner + + +# Fixture for explicitly running in Kernel instance. +@pytest.fixture(scope='session') +def kernel(request): + """Provide a Curio Kernel object for running co-routines.""" + k = Kernel(debug=[longblock, logcrash]) + m = monitor.Monitor(k) + request.addfinalizer(lambda: k.run(shutdown=True)) + request.addfinalizer(m.close) + return k diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curio-1.2/setup.py new/curio-1.4/setup.py --- old/curio-1.2/setup.py 2020-04-07 03:43:23.000000000 +0200 +++ new/curio-1.4/setup.py 2020-08-24 11:41:46.000000000 +0200 @@ -14,7 +14,7 @@ description="Curio", long_description=long_description, license="BSD", - version="1.2", + version="1.4", author="David Beazley", author_email="d...@dabeaz.com", maintainer="David Beazley", @@ -26,7 +26,9 @@ 'test': tests_require, }, python_requires='>= 3.6', - entry_points={"pytest11": ["curio = curio.pytest_plugin"]}, + # This is disabled because it often causes interference with other testing + # plugins people have written. Curio doesn't use it for it's own testing. + # entry_points={"pytest11": ["curio = curio.pytest_plugin"]}, classifiers=[ 'Programming Language :: Python :: 3', "Framework :: Pytest", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curio-1.2/tests/test_io.py new/curio-1.4/tests/test_io.py --- old/curio-1.2/tests/test_io.py 2020-04-07 03:43:23.000000000 +0200 +++ new/curio-1.4/tests/test_io.py 2020-08-24 11:41:46.000000000 +0200 @@ -795,8 +795,8 @@ 'handler done' ] -@pytest.mark.skipif(sys.platform.startswith("win"), - reason="fails on windows") +@pytest.mark.skipif(True, + reason="flaky") def test_sendall_cancel(kernel, portno): done = Event() start = Event() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curio-1.2/tests/test_sync.py new/curio-1.4/tests/test_sync.py --- old/curio-1.2/tests/test_sync.py 2020-04-07 03:43:23.000000000 +0200 +++ new/curio-1.4/tests/test_sync.py 2020-08-24 11:41:46.000000000 +0200 @@ -843,10 +843,79 @@ False ] +class TestResult: + def test_value(self, kernel): + + async def work(x, y, r): + await r.set_value(x+y) + async def main(): + r = Result() + await spawn(work, 2, 3, r) + assert await r.unwrap() == 5 + + kernel.run(main) + + def test_error(self, kernel): + async def work(x, y, r): + try: + await r.set_value(x+y) + except Exception as err: + await r.set_exception(err) + + async def main(): + r = Result() + await spawn(work, 2, "3", r) + with pytest.raises(TypeError): + await r.unwrap() + + kernel.run(main) + + +class TestUniversalResult: + def test_universal_value(self, kernel): + + def work(x, y, r): + r.set_value(x+y) + + async def main(r1, r2): + value = await r1.unwrap() + await r2.set_value(value) + + r1 = UniversalResult() + r2 = UniversalResult() + r3 = UniversalResult() + threading.Thread(target=work, args=[2,3,r1]).start() + threading.Thread(target=asyncio.run, args=[main(r1, r2)]).start() + kernel.run(main, r2, r3) + assert r3.unwrap() == 5 + + def test_universal_error(self, kernel): + + def work(x, y, r): + try: + r.set_value(x+y) + except Exception as err: + r.set_exception(err) + + async def main(r1, r2): + try: + value = await r1.unwrap() + await r2.set_value(value) + except Exception as err: + await r2.set_exception(err) + + r1 = UniversalResult() + r2 = UniversalResult() + r3 = UniversalResult() + threading.Thread(target=work, args=[2,"3",r1]).start() + threading.Thread(target=asyncio.run, args=[main(r1, r2)]).start() + kernel.run(main, r2, r3) + with pytest.raises(TypeError): + val = r3.unwrap() def test_repr(): # For test coverage - for cls in [Lock, Event, Semaphore, Condition, RLock, UniversalEvent ]: + for cls in [Lock, Event, Semaphore, Condition, RLock, UniversalEvent, Result, UniversalResult ]: repr(cls())