diff --git a/.travis.yml b/.travis.yml index 096eade..d0bf9b4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,12 @@ dist: trusty language: python python: - - "3.3" - - "3.4" - "3.5" - "3.6" + - "3.7" + - "3.8" + - "3.9" + - "3.10" install: - pip install -U setuptools - pip install -U pip diff --git a/README.rst b/README.rst index ef2c9cf..f527858 100644 --- a/README.rst +++ b/README.rst @@ -28,8 +28,7 @@ Usage counter = 0 @retry - @asyncio.coroutine - def fn(): + async def fn(): global counter counter += 1 @@ -37,9 +36,8 @@ Usage if counter == 1: raise RuntimeError - @asyncio.coroutine - def main(): - yield from fn() + async def main(): + await fn() loop = asyncio.get_event_loop() @@ -50,4 +48,4 @@ Usage loop.close() -Python 3.3+ is required +Python 3.5+ is required diff --git a/async_retrying.py b/async_retrying.py index e7c66c0..dee2389 100644 --- a/async_retrying.py +++ b/async_retrying.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) -__version__ = '0.2.2' +__version__ = "0.3.0" propagate = forever = ... @@ -23,32 +23,27 @@ class ConditionError(Exception): def unpartial(fn): - while hasattr(fn, 'func'): + while hasattr(fn, "func"): fn = fn.func return fn def isexception(obj): - return ( - isinstance(obj, Exception) or - (inspect.isclass(obj) and (issubclass(obj, Exception))) + return isinstance(obj, Exception) or ( + inspect.isclass(obj) and (issubclass(obj, Exception)) ) -@asyncio.coroutine -def callback(attempt, exc, args, kwargs, delay=None, *, loop): +async def callback(attempt, exc, args, kwargs, delay=None, *, loop): if delay is None: - delay = callback.delay + delay = getattr(callback, "delay", 0.5) - yield from asyncio.sleep(attempt * delay, loop=loop) + await asyncio.sleep(attempt * delay) return retry -callback.delay = 0.5 - - def retry( fn=None, *, @@ -65,30 +60,26 @@ def retry( ): def wrapper(fn): @wraps(fn) - @asyncio.coroutine - def wrapped(*fn_args, **fn_kwargs): + async def wrapped(*fn_args, **fn_kwargs): if isinstance(loop, str): assert cls ^ kwargs, 'choose self.loop or kwargs["loop"]' if cls: - _self = getattr(unpartial(fn), '__self__', None) + _self = getattr(unpartial(fn), "__self__", None) if _self is None: - assert fn_args, 'seems not unbound function' + assert fn_args, "seems not unbound function" _self = fn_args[0] _loop = getattr(_self, loop) elif kwargs: _loop = fn_kwargs[loop] elif loop is None: - _loop = asyncio.get_event_loop() + _loop = asyncio.get_running_loop() else: _loop = loop - if ( - timeout is not None and - asyncio.TimeoutError not in retry_exceptions - ): + if timeout is not None and asyncio.TimeoutError not in retry_exceptions: _retry_exceptions = (asyncio.TimeoutError,) + retry_exceptions else: _retry_exceptions = retry_exceptions @@ -126,15 +117,20 @@ def wrapped(*fn_args, **fn_kwargs): if timeout is None: if asyncio.iscoroutinefunction(unpartial(fn)): - ret = yield from ret + ret = await ret else: if not asyncio.iscoroutinefunction(unpartial(fn)): raise ConditionError( - 'Can\'t set timeout for non coroutinefunction', + "Can't set timeout for non coroutinefunction", ) - with async_timeout.timeout(timeout, loop=_loop): - ret = yield from ret + # Note no async_timeout shortcuts here + # because we must keep a loop passed from the outside. + async with async_timeout.Timeout( + _loop.time() + timeout, + loop=_loop, + ): + ret = await ret return ret @@ -143,20 +139,17 @@ def wrapped(*fn_args, **fn_kwargs): except fatal_exceptions: raise except _retry_exceptions as exc: - _attempts = 'infinity' if attempts is forever else attempts + _attempts = "infinity" if attempts is forever else attempts context = { - 'fn': fn, - 'attempt': attempt, - 'attempts': _attempts, + "fn": fn, + "attempt": attempt, + "attempts": _attempts, } - if ( - _loop.get_debug() or - (attempts is not forever and attempt == attempts) - ): - + if attempts is not forever and attempt == attempts: logger.warning( - exc.__class__.__name__ + ' -> Attempts (%(attempt)d) are over for %(fn)r', # noqa + exc.__class__.__name__ + + " -> Attempts (%(attempt)d) are over for %(fn)r", # noqa context, exc_info=exc, ) @@ -170,26 +163,31 @@ def wrapped(*fn_args, **fn_kwargs): ret = fallback(fn_args, fn_kwargs, loop=_loop) if asyncio.iscoroutinefunction(unpartial(fallback)): # noqa - ret = yield from ret + ret = await ret else: ret = fallback return ret logger.debug( - exc.__class__.__name__ + ' -> Tried attempt #%(attempt)d from total %(attempts)s for %(fn)r', # noqa + exc.__class__.__name__ + + " -> Tried attempt #%(attempt)d from total %(attempts)s for %(fn)r", # noqa context, exc_info=exc, ) ret = callback( - attempt, exc, fn_args, fn_kwargs, loop=_loop, + attempt, + exc, + fn_args, + fn_kwargs, + loop=_loop, ) attempt += 1 if asyncio.iscoroutinefunction(unpartial(callback)): - ret = yield from ret + ret = await ret if ret is not retry: return ret diff --git a/requirements.txt b/requirements.txt index cbe0559..898b99a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,29 +1,21 @@ -appnope==0.1.0 -async-timeout==1.2.1 -coverage==4.4.1 -decorator==4.0.11 -flake8==3.3.0 -ipdb==0.10.3 -ipython==6.1.0 -ipython-genutils==0.2.0 -isort==4.2.15 -jedi==0.10.2 -mccabe==0.6.1 -pexpect==4.2.1 -pickleshare==0.7.4 -pluggy==0.4.0 -prompt-toolkit==1.0.14 -ptyprocess==0.5.2 -py==1.4.34 -pycodestyle==2.3.1 -pyflakes==1.5.0 -Pygments==2.2.0 -pytest==3.1.3 -pytest-cov==2.5.1 -pytest-mock==1.6.3 -simplegeneric==0.8.1 -six==1.10.0 -tox==2.7.0 -traitlets==4.3.2 -virtualenv==15.1.0 -wcwidth==0.1.7 +async-timeout==4.0.2 +attrs==22.1.0 +black==22.10.0 +click==8.1.3 +colorama==0.4.6 +coverage==6.5.0 +exceptiongroup==1.0.4 +iniconfig==1.1.1 +mypy==0.991 +mypy-extensions==0.4.3 +packaging==21.3 +pathspec==0.10.2 +platformdirs==2.5.4 +pluggy==1.0.0 +pyparsing==3.0.9 +pytest==7.2.0 +pytest-asyncio==0.20.2 +pytest-cov==4.0.0 +pytest-mock==3.10.0 +tomli==2.0.1 +typing_extensions==4.4.0 \ No newline at end of file diff --git a/setup.py b/setup.py index bf180ea..2e96737 100644 --- a/setup.py +++ b/setup.py @@ -1,52 +1,47 @@ -import io -import os import re +from pathlib import Path from setuptools import setup +home = Path(__file__).parent +readme = home / "README.rst" -def get_version(): - regex = r"__version__\s=\s\'(?P[\d\.]+?)\'" - - path = ('async_retrying.py',) - - return re.search(regex, read(*path)).group('version') - -def read(*parts): - filename = os.path.join(os.path.abspath(os.path.dirname(__file__)), *parts) - - with io.open(filename, encoding='utf-8', mode='rt') as fp: - return fp.read() +def get_version(): + regex = re.compile(r'__version__ = "(?P.+)"', re.M) + match = regex.search((home / "async_retrying.py").read_text()) + return match.group("version") setup( - name='async_retrying', + name="async_retrying", version=get_version(), - author='OCEAN S.A.', - author_email='osf@ocean.io', - url='https://github.com/wikibusiness/async_retrying', - description='Simple retrying for asyncio', - long_description=read('README.rst'), + author="OCEAN S.A.", + author_email="osf@ocean.io", + url="https://github.com/wikibusiness/async_retrying", + description="Simple retrying for asyncio", + long_description=readme.read_text(), install_requires=[ - 'async_timeout', + "async_timeout", ], extras_require={ - ':python_version=="3.3"': ['asyncio'], + ':python_version=="3.5"': ["asyncio"], }, - py_modules=['async_retrying'], + py_modules=["async_retrying"], include_package_data=True, zip_safe=False, classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", ], - keywords=['asyncio', 'retrying'], + keywords=["asyncio", "retrying"], ) diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index c34c1fe..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,49 +0,0 @@ -import asyncio - -import pytest - - -@pytest.fixture -def loop(request): - with pytest.raises(RuntimeError): - asyncio.get_event_loop() - - loop = asyncio.new_event_loop() - - loop.set_debug(False) - - request.addfinalizer(lambda: asyncio.set_event_loop(None)) - - yield loop - - loop.call_soon(loop.stop) - loop.run_forever() - loop.close() - - -@pytest.mark.tryfirst -def pytest_pycollect_makeitem(collector, name, obj): - if collector.funcnamefilter(name): - item = pytest.Function(name, parent=collector) - - if 'run_loop' in item.keywords: - return list(collector._genfunctions(name, obj)) - - -@pytest.mark.tryfirst -def pytest_pyfunc_call(pyfuncitem): - if 'run_loop' in pyfuncitem.keywords: - funcargs = pyfuncitem.funcargs - - loop = funcargs['loop'] - - testargs = { - arg: funcargs[arg] - for arg in pyfuncitem._fixtureinfo.argnames - } - - assert asyncio.iscoroutinefunction(pyfuncitem.obj) - - loop.run_until_complete(pyfuncitem.obj(**testargs)) - - return True diff --git a/tests/test_async_retrying.py b/tests/test_async_retrying.py index a42bf21..f76c2f9 100644 --- a/tests/test_async_retrying.py +++ b/tests/test_async_retrying.py @@ -1,43 +1,39 @@ import asyncio - from functools import partial +from unittest.mock import call import pytest from async_retrying import callback, retry -@pytest.mark.run_loop -@asyncio.coroutine -def test_smoke(loop): +@pytest.mark.asyncio +async def test_smoke(event_loop): counter = 0 - @retry(loop=loop) - @asyncio.coroutine - def fn(): + @retry(loop=event_loop) + async def fn(): nonlocal counter counter += 1 if counter == 1: - raise RuntimeError + raise return counter - ret = yield from partial(fn)() + ret = await partial(fn)() assert ret == counter -@pytest.mark.run_loop -@asyncio.coroutine -def test_callback_delay(mocker, loop): - mocker.patch('asyncio.sleep') +@pytest.mark.asyncio +async def test_callback_delay(mocker, event_loop): + mocker.patch("asyncio.sleep") counter = 0 - @retry(callback=partial(callback, delay=5), loop=loop) - @asyncio.coroutine - def fn(): + @retry(callback=partial(callback, delay=5), loop=event_loop) + async def fn(): nonlocal counter counter += 1 @@ -47,13 +43,10 @@ def fn(): return counter - ret = yield from partial(fn)() + ret = await partial(fn)() assert ret == counter - expected = [ - ((5,), {'loop': loop}), - ((10,), {'loop': loop}), - ] + expected = [call(5), call(10)] assert asyncio.sleep.call_args_list == expected diff --git a/tests/test_condition_error.py b/tests/test_condition_error.py index 355891f..6d01580 100644 --- a/tests/test_condition_error.py +++ b/tests/test_condition_error.py @@ -1,17 +1,14 @@ -import asyncio - import pytest from async_retrying import ConditionError, retry -@pytest.mark.run_loop -@asyncio.coroutine -def test_timeout_is_not_none_and_not_async(loop): +@pytest.mark.asyncio +async def test_timeout_is_not_none_and_not_async(event_loop): - @retry(timeout=0.5, loop=loop) + @retry(timeout=0.5, loop=event_loop) def not_coro(): pass with pytest.raises(ConditionError): - yield from not_coro() + await not_coro() diff --git a/tests/test_loop.py b/tests/test_loop.py index b0dd782..e583abb 100644 --- a/tests/test_loop.py +++ b/tests/test_loop.py @@ -1,18 +1,16 @@ -import pytest import asyncio -from async_retrying import retry, RetryError +import pytest +from async_retrying import RetryError, retry -@pytest.mark.run_loop -@asyncio.coroutine -def test_immutable_with_kwargs(loop): - @retry(loop='_loop', immutable=True, kwargs=True, fatal_exceptions=KeyError) - @asyncio.coroutine - def coro(a, *, _loop): - a.pop('a') +@pytest.mark.asyncio +async def test_immutable_with_kwargs(event_loop): + @retry(loop="_loop", immutable=True, kwargs=True, fatal_exceptions=KeyError) + async def coro(a, *, _loop): + a.pop("a") raise RuntimeError with pytest.raises(RetryError): - yield from coro(a={'a': 'a'}, _loop=loop) + await coro(a={"a": "a"}, _loop=event_loop) diff --git a/tox.ini b/tox.ini index 9858674..a4ec9da 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py3{3,4,5,6} + py3{5,6,7,8,9,10} skip_missing_interpreters = True [testenv]