From 628af72b72eb30a92ed02f4787a23f728acd5903 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Sat, 5 May 2018 13:05:27 +0300 Subject: [PATCH 1/4] Bump to 3.0 --- .gitignore | 3 +++ .travis.yml | 6 +---- CHANGES.rst | 7 ++++++ Makefile | 10 ++++++++ async_timeout/__init__.py | 53 +++++++++++++++++++++++---------------- async_timeout/py.typed | 1 + requirements.txt | 1 + setup.cfg | 6 +++++ setup.py | 5 ++-- 9 files changed, 64 insertions(+), 28 deletions(-) create mode 100644 Makefile create mode 100644 async_timeout/py.typed diff --git a/.gitignore b/.gitignore index 72364f9..d4edc9d 100644 --- a/.gitignore +++ b/.gitignore @@ -87,3 +87,6 @@ ENV/ # Rope project settings .ropeproject + +.mypy_cache +.pytest_cache \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index be8c08c..85bde36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,11 +9,7 @@ install: - pip install codecov script: - - pytest tests - - python setup.py check -rm - - if python -c "import sys; sys.exit(sys.version_info < (3,5))"; then - python setup.py check -s; - fi + - make test after_success: diff --git a/CHANGES.rst b/CHANGES.rst index 17f3e21..b004816 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,13 @@ CHANGES ======= +3.0.0 (2018-05-05) +------------------ + +- Drop Python 3.4, the minimal supported version is Python 3.5.3 + +- Provide type annotations + 2.0.1 (2018-03-13) ------------------ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1bd1cbd --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +test: mypy check + pytest tests + + +mypy: + mypy async_timeout tests + + +check: + python setup.py check -rms diff --git a/async_timeout/__init__.py b/async_timeout/__init__.py index fc621c6..08bdd47 100644 --- a/async_timeout/__init__.py +++ b/async_timeout/__init__.py @@ -1,8 +1,11 @@ import asyncio import sys +from types import TracebackType +from typing import Optional, Type -__version__ = '2.0.1' + +__version__ = '3.0.0' PY_37 = sys.version_info >= (3, 7) @@ -21,42 +24,48 @@ class timeout: timeout - value in seconds or None to disable timeout logic loop - asyncio compatible event loop """ - def __init__(self, timeout, *, loop=None): + def __init__(self, timeout: Optional[float], + *, loop: asyncio.AbstractEventLoop=None) -> None: self._timeout = timeout if loop is None: loop = asyncio.get_event_loop() self._loop = loop - self._task = None + self._task = None # type: Optional[asyncio.Task] self._cancelled = False - self._cancel_handler = None - self._cancel_at = None + self._cancel_handler = None # type: Optional[asyncio.Handle] + self._cancel_at = None # type: Optional[float] - def __enter__(self): + def __enter__(self) -> 'timeout': return self._do_enter() - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__(self, + exc_type: Type[BaseException], + exc_val: BaseException, + exc_tb: TracebackType) -> Optional[bool]: self._do_exit(exc_type) + return None - @asyncio.coroutine - def __aenter__(self): + async def __aenter__(self) -> 'timeout': return self._do_enter() - @asyncio.coroutine - def __aexit__(self, exc_type, exc_val, exc_tb): + async def __aexit__(self, + exc_type: Type[BaseException], + exc_val: BaseException, + exc_tb: TracebackType) -> None: self._do_exit(exc_type) @property - def expired(self): + def expired(self) -> bool: return self._cancelled @property - def remaining(self): + def remaining(self) -> Optional[float]: if self._cancel_at is not None: return max(self._cancel_at - self._loop.time(), 0.0) else: return None - def _do_enter(self): + def _do_enter(self) -> 'timeout': # Support Tornado 5- without timeout # Details: https://github.com/python/asyncio/issues/392 if self._timeout is None: @@ -76,7 +85,7 @@ def _do_enter(self): self._cancel_at, self._cancel_task) return self - def _do_exit(self, exc_type): + def _do_exit(self, exc_type: Type[BaseException]) -> None: if exc_type is asyncio.CancelledError and self._cancelled: self._cancel_handler = None self._task = None @@ -85,20 +94,22 @@ def _do_exit(self, exc_type): self._cancel_handler.cancel() self._cancel_handler = None self._task = None + return None - def _cancel_task(self): - self._task.cancel() - self._cancelled = True + def _cancel_task(self) -> None: + if self._task is not None: + self._task.cancel() + self._cancelled = True -def current_task(loop): +def current_task(loop: asyncio.AbstractEventLoop) -> asyncio.Task: if PY_37: - task = asyncio.current_task(loop=loop) + task = asyncio.current_task(loop=loop) # type: ignore else: task = asyncio.Task.current_task(loop=loop) if task is None: # this should be removed, tokio must use register_task and family API if hasattr(loop, 'current_task'): - task = loop.current_task() + task = loop.current_task() # type: ignore return task diff --git a/async_timeout/py.typed b/async_timeout/py.typed new file mode 100644 index 0000000..f6e0339 --- /dev/null +++ b/async_timeout/py.typed @@ -0,0 +1 @@ +Placeholder \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7db227f..ee0f361 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ pytest==3.5.0 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 docutils==0.14 +mypy==0.600 -e . diff --git a/setup.cfg b/setup.cfg index a57c7ae..919fd65 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,8 @@ [tool:pytest] addopts= --cov=async_timeout --cov-report=term --cov-report=html --cov-branch + +[metadata] +license_file = LICENSE + +[mypy-pytest] +ignore_missing_imports = true diff --git a/setup.py b/setup.py index 9740a44..37ced2b 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ import pathlib import re +import sys from setuptools import setup @@ -29,7 +30,6 @@ def read(name): 'Intended Audience :: Developers', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Topic :: Internet :: WWW/HTTP', @@ -40,4 +40,5 @@ def read(name): url='https://github.com/aio-libs/async_timeout/', license='Apache 2', packages=['async_timeout'], - include_package_data=False) + python_requires='>=3.5.3', + include_package_data=True) From 247f469fe7d739fbd75effd72eb314eb5ebfcde1 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Sat, 5 May 2018 13:16:57 +0300 Subject: [PATCH 2/4] Replace pytest-aiohttp with pytest-asyncio --- .travis.yml | 2 +- requirements.txt | 2 +- tests/conftest.py | 7 --- tests/test_py35.py | 2 + tests/test_timeout.py | 126 +++++++++++++++++------------------------- 5 files changed, 56 insertions(+), 83 deletions(-) diff --git a/.travis.yml b/.travis.yml index 85bde36..68a923b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,4 +25,4 @@ deploy: on: tags: true all_branches: true - python: 3.5 + python: 3.6 diff --git a/requirements.txt b/requirements.txt index ee0f361..061e985 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ pytest==3.5.0 -pytest-aiohttp==0.3.0 +pytest-asyncio==0.8.0 pytest-cov==2.5.1 docutils==0.14 mypy==0.600 diff --git a/tests/conftest.py b/tests/conftest.py index 0ec19b3..e69de29 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +0,0 @@ -import sys - - -def pytest_ignore_collect(path, config): - if 'py35' in str(path): - if sys.version_info < (3, 5, 0): - return True diff --git a/tests/test_py35.py b/tests/test_py35.py index d3410db..00bb7f0 100644 --- a/tests/test_py35.py +++ b/tests/test_py35.py @@ -4,6 +4,8 @@ from async_timeout import timeout +pytestmark = pytest.mark.asyncio + async def test_async_timeout(loop): with pytest.raises(asyncio.TimeoutError): diff --git a/tests/test_timeout.py b/tests/test_timeout.py index 04e86f0..34d43da 100644 --- a/tests/test_timeout.py +++ b/tests/test_timeout.py @@ -12,6 +12,9 @@ ensure_future = asyncio.async +pytestmark = pytest.mark.asyncio + + def create_future(loop): """Compatibility wrapper for the loop.create_future() call introduced in 3.5.2.""" @@ -21,14 +24,12 @@ def create_future(loop): return asyncio.Future(loop=loop) -@asyncio.coroutine -def test_timeout(loop): +async def test_timeout(loop): canceled_raised = False - @asyncio.coroutine - def long_running_task(): + async def long_running_task(): try: - yield from asyncio.sleep(10, loop=loop) + await asyncio.sleep(10, loop=loop) except asyncio.CancelledError: nonlocal canceled_raised canceled_raised = True @@ -36,45 +37,40 @@ def long_running_task(): with pytest.raises(asyncio.TimeoutError): with timeout(0.01, loop=loop) as t: - yield from long_running_task() + await long_running_task() assert t._loop is loop assert canceled_raised, 'CancelledError was not raised' -@asyncio.coroutine -def test_timeout_finish_in_time(loop): - @asyncio.coroutine - def long_running_task(): - yield from asyncio.sleep(0.01, loop=loop) +async def test_timeout_finish_in_time(loop): + async def long_running_task(): + await asyncio.sleep(0.01, loop=loop) return 'done' with timeout(0.1, loop=loop): - resp = yield from long_running_task() + resp = await long_running_task() assert resp == 'done' def test_timeout_global_loop(loop): asyncio.set_event_loop(loop) - @asyncio.coroutine - def run(): + async def run(): with timeout(10) as t: - yield from asyncio.sleep(0.01) + await asyncio.sleep(0.01) assert t._loop is loop loop.run_until_complete(run()) -@asyncio.coroutine -def test_timeout_disable(loop): - @asyncio.coroutine - def long_running_task(): - yield from asyncio.sleep(0.1, loop=loop) +async def test_timeout_disable(loop): + async def long_running_task(): + await asyncio.sleep(0.1, loop=loop) return 'done' t0 = loop.time() with timeout(None, loop=loop): - resp = yield from long_running_task() + resp = await long_running_task() assert resp == 'done' dt = loop.time() - t0 assert 0.09 < dt < 0.13, dt @@ -85,72 +81,63 @@ def test_timeout_is_none_no_task(loop): assert cm._task is None -@asyncio.coroutine -def test_timeout_enable_zero(loop): +async def test_timeout_enable_zero(loop): with pytest.raises(asyncio.TimeoutError): with timeout(0, loop=loop) as cm: - yield from asyncio.sleep(0.1, loop=loop) + await asyncio.sleep(0.1, loop=loop) assert cm.expired -@asyncio.coroutine -def test_timeout_enable_zero_coro_not_started(loop): +async def test_timeout_enable_zero_coro_not_started(loop): coro_started = False - @asyncio.coroutine - def coro(): + async def coro(): nonlocal coro_started coro_started = True with pytest.raises(asyncio.TimeoutError): with timeout(0, loop=loop) as cm: - yield from asyncio.sleep(0, loop=loop) - yield from coro() + await asyncio.sleep(0, loop=loop) + await coro() assert cm.expired assert coro_started is False -@asyncio.coroutine -def test_timeout_not_relevant_exception(loop): - yield from asyncio.sleep(0, loop=loop) +async def test_timeout_not_relevant_exception(loop): + await asyncio.sleep(0, loop=loop) with pytest.raises(KeyError): with timeout(0.1, loop=loop): raise KeyError -@asyncio.coroutine -def test_timeout_canceled_error_is_not_converted_to_timeout(loop): - yield from asyncio.sleep(0, loop=loop) +async def test_timeout_canceled_error_is_not_converted_to_timeout(loop): + await asyncio.sleep(0, loop=loop) with pytest.raises(asyncio.CancelledError): with timeout(0.001, loop=loop): raise asyncio.CancelledError -@asyncio.coroutine -def test_timeout_blocking_loop(loop): - @asyncio.coroutine - def long_running_task(): +async def test_timeout_blocking_loop(loop): + async def long_running_task(): time.sleep(0.1) return 'done' with timeout(0.01, loop=loop): - result = yield from long_running_task() + result = await long_running_task() assert result == 'done' -@asyncio.coroutine -def test_for_race_conditions(loop): +async def test_for_race_conditions(loop): fut = create_future(loop) loop.call_later(0.1, fut.set_result('done')) with timeout(0.2, loop=loop): - resp = yield from fut + resp = await fut assert resp == 'done' -@asyncio.coroutine -def test_timeout_time(loop): +async def test_timeout_time(loop): foo_running = None start = loop.time() @@ -158,7 +145,7 @@ def test_timeout_time(loop): with timeout(0.1, loop=loop): foo_running = True try: - yield from asyncio.sleep(0.2, loop=loop) + await asyncio.sleep(0.2, loop=loop) finally: foo_running = False @@ -175,79 +162,70 @@ def test_raise_runtimeerror_if_no_task(loop): pass -@asyncio.coroutine -def test_outer_coro_is_not_cancelled(loop): +async def test_outer_coro_is_not_cancelled(loop): has_timeout = False - @asyncio.coroutine - def outer(): + async def outer(): nonlocal has_timeout try: with timeout(0.001, loop=loop): - yield from asyncio.sleep(1, loop=loop) + await asyncio.sleep(1, loop=loop) except asyncio.TimeoutError: has_timeout = True task = ensure_future(outer(), loop=loop) - yield from task + await task assert has_timeout assert not task.cancelled() assert task.done() -@asyncio.coroutine -def test_cancel_outer_coro(loop): +async def test_cancel_outer_coro(loop): fut = create_future(loop) - @asyncio.coroutine - def outer(): + async def outer(): fut.set_result(None) - yield from asyncio.sleep(1, loop=loop) + await asyncio.sleep(1, loop=loop) task = ensure_future(outer(), loop=loop) - yield from fut + await fut task.cancel() with pytest.raises(asyncio.CancelledError): - yield from task + await task assert task.cancelled() assert task.done() -@asyncio.coroutine -def test_timeout_suppress_exception_chain(loop): +async def test_timeout_suppress_exception_chain(loop): with pytest.raises(asyncio.TimeoutError) as ctx: with timeout(0.01, loop=loop): - yield from asyncio.sleep(10, loop=loop) + await asyncio.sleep(10, loop=loop) assert not ctx.value.__suppress_context__ -@asyncio.coroutine -def test_timeout_expired(loop): +async def test_timeout_expired(loop): with pytest.raises(asyncio.TimeoutError): with timeout(0.01, loop=loop) as cm: - yield from asyncio.sleep(10, loop=loop) + await asyncio.sleep(10, loop=loop) assert cm.expired -@asyncio.coroutine -def test_timeout_inner_timeout_error(loop): +async def test_timeout_inner_timeout_error(loop): with pytest.raises(asyncio.TimeoutError): with timeout(0.01, loop=loop) as cm: raise asyncio.TimeoutError assert not cm.expired -@asyncio.coroutine -def test_timeout_inner_other_error(loop): +async def test_timeout_inner_other_error(loop): with pytest.raises(RuntimeError): with timeout(0.01, loop=loop) as cm: raise RuntimeError assert not cm.expired -@asyncio.coroutine -def test_timeout_remaining(loop): +async def test_timeout_remaining(loop): with timeout(None, loop=loop) as cm: assert cm.remaining is None @@ -255,11 +233,11 @@ def test_timeout_remaining(loop): assert t.remaining is None with timeout(1.0, loop=loop) as cm: - yield from asyncio.sleep(0.1, loop=loop) + await asyncio.sleep(0.1, loop=loop) assert cm.remaining < 1.0 with pytest.raises(asyncio.TimeoutError): with timeout(0.1, loop=loop) as cm: - yield from asyncio.sleep(0.5, loop=loop) + await asyncio.sleep(0.5, loop=loop) assert cm.remaining == 0.0 From 057a247917717418d9d43ad8c4aa0c3d049c5eb8 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Sat, 5 May 2018 13:23:46 +0300 Subject: [PATCH 3/4] Fix tests --- tests/conftest.py | 6 ++++++ tests/test_timeout.py | 20 +++++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e69de29..9a43bc0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -0,0 +1,6 @@ +import pytest + + +@pytest.fixture +def loop(event_loop): + return event_loop diff --git a/tests/test_timeout.py b/tests/test_timeout.py index 34d43da..75bf145 100644 --- a/tests/test_timeout.py +++ b/tests/test_timeout.py @@ -12,9 +12,6 @@ ensure_future = asyncio.async -pytestmark = pytest.mark.asyncio - - def create_future(loop): """Compatibility wrapper for the loop.create_future() call introduced in 3.5.2.""" @@ -24,6 +21,7 @@ def create_future(loop): return asyncio.Future(loop=loop) +@pytest.mark.asyncio async def test_timeout(loop): canceled_raised = False @@ -42,6 +40,7 @@ async def long_running_task(): assert canceled_raised, 'CancelledError was not raised' +@pytest.mark.asyncio async def test_timeout_finish_in_time(loop): async def long_running_task(): await asyncio.sleep(0.01, loop=loop) @@ -63,6 +62,7 @@ async def run(): loop.run_until_complete(run()) +@pytest.mark.asyncio async def test_timeout_disable(loop): async def long_running_task(): await asyncio.sleep(0.1, loop=loop) @@ -81,6 +81,7 @@ def test_timeout_is_none_no_task(loop): assert cm._task is None +@pytest.mark.asyncio async def test_timeout_enable_zero(loop): with pytest.raises(asyncio.TimeoutError): with timeout(0, loop=loop) as cm: @@ -89,6 +90,7 @@ async def test_timeout_enable_zero(loop): assert cm.expired +@pytest.mark.asyncio async def test_timeout_enable_zero_coro_not_started(loop): coro_started = False @@ -105,6 +107,7 @@ async def coro(): assert coro_started is False +@pytest.mark.asyncio async def test_timeout_not_relevant_exception(loop): await asyncio.sleep(0, loop=loop) with pytest.raises(KeyError): @@ -112,6 +115,7 @@ async def test_timeout_not_relevant_exception(loop): raise KeyError +@pytest.mark.asyncio async def test_timeout_canceled_error_is_not_converted_to_timeout(loop): await asyncio.sleep(0, loop=loop) with pytest.raises(asyncio.CancelledError): @@ -119,6 +123,7 @@ async def test_timeout_canceled_error_is_not_converted_to_timeout(loop): raise asyncio.CancelledError +@pytest.mark.asyncio async def test_timeout_blocking_loop(loop): async def long_running_task(): time.sleep(0.1) @@ -129,6 +134,7 @@ async def long_running_task(): assert result == 'done' +@pytest.mark.asyncio async def test_for_race_conditions(loop): fut = create_future(loop) loop.call_later(0.1, fut.set_result('done')) @@ -137,6 +143,7 @@ async def test_for_race_conditions(loop): assert resp == 'done' +@pytest.mark.asyncio async def test_timeout_time(loop): foo_running = None @@ -162,6 +169,7 @@ def test_raise_runtimeerror_if_no_task(loop): pass +@pytest.mark.asyncio async def test_outer_coro_is_not_cancelled(loop): has_timeout = False @@ -181,6 +189,7 @@ async def outer(): assert task.done() +@pytest.mark.asyncio async def test_cancel_outer_coro(loop): fut = create_future(loop) @@ -197,6 +206,7 @@ async def outer(): assert task.done() +@pytest.mark.asyncio async def test_timeout_suppress_exception_chain(loop): with pytest.raises(asyncio.TimeoutError) as ctx: with timeout(0.01, loop=loop): @@ -204,6 +214,7 @@ async def test_timeout_suppress_exception_chain(loop): assert not ctx.value.__suppress_context__ +@pytest.mark.asyncio async def test_timeout_expired(loop): with pytest.raises(asyncio.TimeoutError): with timeout(0.01, loop=loop) as cm: @@ -211,6 +222,7 @@ async def test_timeout_expired(loop): assert cm.expired +@pytest.mark.asyncio async def test_timeout_inner_timeout_error(loop): with pytest.raises(asyncio.TimeoutError): with timeout(0.01, loop=loop) as cm: @@ -218,6 +230,7 @@ async def test_timeout_inner_timeout_error(loop): assert not cm.expired +@pytest.mark.asyncio async def test_timeout_inner_other_error(loop): with pytest.raises(RuntimeError): with timeout(0.01, loop=loop) as cm: @@ -225,6 +238,7 @@ async def test_timeout_inner_other_error(loop): assert not cm.expired +@pytest.mark.asyncio async def test_timeout_remaining(loop): with timeout(None, loop=loop) as cm: assert cm.remaining is None From 5c6913e27b903ca1e7f9d905fa06df9b00e509c8 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Sat, 5 May 2018 13:30:44 +0300 Subject: [PATCH 4/4] More tests --- tests/test_timeout.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_timeout.py b/tests/test_timeout.py index 75bf145..2fb9919 100644 --- a/tests/test_timeout.py +++ b/tests/test_timeout.py @@ -255,3 +255,9 @@ async def test_timeout_remaining(loop): await asyncio.sleep(0.5, loop=loop) assert cm.remaining == 0.0 + + +def test_cancel_without_starting(loop): + tm = timeout(1, loop=loop) + tm._cancel_task() + tm._cancel_task() # double call should success