From e206e5854e7b3e4744d33c1f561846357edc8416 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Fri, 3 Sep 2021 20:49:53 +0100 Subject: [PATCH 1/5] Add clean fix for cancelation on 3.9+. --- async_timeout/__init__.py | 25 ++++++++++++++++++------- tests/test_timeout.py | 9 +++------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/async_timeout/__init__.py b/async_timeout/__init__.py index 92e660d..6420bd2 100644 --- a/async_timeout/__init__.py +++ b/async_timeout/__init__.py @@ -14,6 +14,9 @@ __all__ = ("timeout", "timeout_at") +_SENTINEL = object() + + def timeout(delay: Optional[float]) -> "Timeout": """timeout context manager. @@ -112,7 +115,7 @@ def __exit__( exc_val: BaseException, exc_tb: TracebackType, ) -> Optional[bool]: - self._do_exit(exc_type) + self._do_exit(exc_type, exc_val) return None async def __aenter__(self) -> "Timeout": @@ -125,7 +128,7 @@ async def __aexit__( exc_val: BaseException, exc_tb: TracebackType, ) -> Optional[bool]: - self._do_exit(exc_type) + self._do_exit(exc_type, exc_val) return None @property @@ -188,20 +191,28 @@ def _do_enter(self) -> None: raise RuntimeError("invalid state {}".format(self._state.value)) self._state = _State.ENTER - def _do_exit(self, exc_type: Type[BaseException]) -> None: - if exc_type is asyncio.CancelledError and self._state == _State.TIMEOUT: + def _do_exit(self, exc_type: Type[BaseException], exc_val: BaseException) -> None: + if sys.version_info >= (3, 9): + was_timeout_cancelled = lambda: _SENTINEL in exc_val.args + else: + was_timeout_cancelled = lambda: self._state == _State.TIMEOUT + if exc_type is asyncio.CancelledError and was_timeout_cancelled(): self._timeout_handler = None raise asyncio.TimeoutError - # timeout is not expired + # timeout has not expired self._state = _State.EXIT self._reject() return None def _on_timeout(self, task: "asyncio.Task[None]") -> None: # See Issue #229 and PR #230 for details - if task._fut_waiter and task._fut_waiter.cancelled(): # type: ignore[attr-defined] # noqa: E501 + if sys.version_info < (3, 9) and task._fut_waiter and task._fut_waiter.cancelled(): # type: ignore[attr-defined] # noqa: E501 return - task.cancel() + + if sys.version_info >= (3, 9): + task.cancel(_SENTINEL) + else: + task.cancel() self._state = _State.TIMEOUT diff --git a/tests/test_timeout.py b/tests/test_timeout.py index f006ff7..25caee3 100644 --- a/tests/test_timeout.py +++ b/tests/test_timeout.py @@ -372,11 +372,8 @@ async def test_task(deadline: float, loop: asyncio.AbstractEventLoop) -> None: with pytest.raises(asyncio.CancelledError): await t - -@pytest.mark.xfail( - reason="The test is CPU performance sensitive, might fail on slow CI box" -) -@pytest.mark.skipif(sys.version_info < (3, 7), reason="Not supported in 3.6") +@pytest.mark.xfail(reason="Can't see a way to fix this currently.") +@pytest.mark.skipif(sys.version_info < (3, 9), reason="Can't be fixed in <3.9.") @pytest.mark.asyncio async def test_race_condition_cancel_after() -> None: """Test race condition when cancelling after timeout. @@ -396,7 +393,7 @@ async def test_task(deadline: float, loop: asyncio.AbstractEventLoop) -> None: loop = asyncio.get_running_loop() deadline = loop.time() + 1 t = asyncio.create_task(test_task(deadline, loop)) - loop.call_at(deadline + 0.0000000000001, t.cancel) + loop.call_at(deadline + 0.000001, t.cancel) # If we get a TimeoutError, then the code is broken. with pytest.raises(asyncio.CancelledError): await t From 609b4694845bc9fe89a73cf479458925b1d2391f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 3 Sep 2021 19:50:33 +0000 Subject: [PATCH 2/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .gitignore | 2 +- async_timeout/__init__.py | 4 ++-- async_timeout/py.typed | 2 +- requirements.txt | 12 ++++++------ tests/test_timeout.py | 1 + 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index d4edc9d..b4a8eb1 100644 --- a/.gitignore +++ b/.gitignore @@ -89,4 +89,4 @@ ENV/ .ropeproject .mypy_cache -.pytest_cache \ No newline at end of file +.pytest_cache diff --git a/async_timeout/__init__.py b/async_timeout/__init__.py index 6420bd2..d583ee5 100644 --- a/async_timeout/__init__.py +++ b/async_timeout/__init__.py @@ -145,7 +145,7 @@ def reject(self) -> None: # cancel is maybe better name but # task.cancel() raises CancelledError in asyncio world. if self._state not in (_State.INIT, _State.ENTER): - raise RuntimeError("invalid state {}".format(self._state.value)) + raise RuntimeError(f"invalid state {self._state.value}") self._reject() def _reject(self) -> None: @@ -188,7 +188,7 @@ def shift_to(self, deadline: float) -> None: def _do_enter(self) -> None: if self._state != _State.INIT: - raise RuntimeError("invalid state {}".format(self._state.value)) + raise RuntimeError(f"invalid state {self._state.value}") self._state = _State.ENTER def _do_exit(self, exc_type: Type[BaseException], exc_val: BaseException) -> None: diff --git a/async_timeout/py.typed b/async_timeout/py.typed index f6e0339..3b94f91 100644 --- a/async_timeout/py.typed +++ b/async_timeout/py.typed @@ -1 +1 @@ -Placeholder \ No newline at end of file +Placeholder diff --git a/requirements.txt b/requirements.txt index 83757c4..f308a3b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ -isort==5.9.3 +-e . black==21.8b0; implementation_name=="cpython" -pytest==6.2.5 -pytest-asyncio==0.15.1 -pytest-cov==2.12.1 docutils==0.17.1 -mypy==0.910; implementation_name=="cpython" flake8==3.9.2 +isort==5.9.3 +mypy==0.910; implementation_name=="cpython" pre-commit==2.15 --e . +pytest==6.2.5 +pytest-asyncio==0.15.1 +pytest-cov==2.12.1 diff --git a/tests/test_timeout.py b/tests/test_timeout.py index 25caee3..e86f9ba 100644 --- a/tests/test_timeout.py +++ b/tests/test_timeout.py @@ -372,6 +372,7 @@ async def test_task(deadline: float, loop: asyncio.AbstractEventLoop) -> None: with pytest.raises(asyncio.CancelledError): await t + @pytest.mark.xfail(reason="Can't see a way to fix this currently.") @pytest.mark.skipif(sys.version_info < (3, 9), reason="Can't be fixed in <3.9.") @pytest.mark.asyncio From 6e985e15a5156d1c716dd2b08210bcfbdbd85e2c Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Fri, 3 Sep 2021 20:52:03 +0100 Subject: [PATCH 3/5] Use function. --- async_timeout/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/async_timeout/__init__.py b/async_timeout/__init__.py index 6420bd2..b271a90 100644 --- a/async_timeout/__init__.py +++ b/async_timeout/__init__.py @@ -193,9 +193,11 @@ def _do_enter(self) -> None: def _do_exit(self, exc_type: Type[BaseException], exc_val: BaseException) -> None: if sys.version_info >= (3, 9): - was_timeout_cancelled = lambda: _SENTINEL in exc_val.args + def was_timeout_cancelled(): + return _SENTINEL in exc_val.args else: - was_timeout_cancelled = lambda: self._state == _State.TIMEOUT + def was_timeout_cancelled(): + return self._state == _State.TIMEOUT if exc_type is asyncio.CancelledError and was_timeout_cancelled(): self._timeout_handler = None raise asyncio.TimeoutError From 8e716a0f555b15a9b8bd3e2e93ff5276d8b2f24f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 3 Sep 2021 19:52:42 +0000 Subject: [PATCH 4/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- async_timeout/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/async_timeout/__init__.py b/async_timeout/__init__.py index ddc6cbe..8ae90ab 100644 --- a/async_timeout/__init__.py +++ b/async_timeout/__init__.py @@ -193,11 +193,15 @@ def _do_enter(self) -> None: def _do_exit(self, exc_type: Type[BaseException], exc_val: BaseException) -> None: if sys.version_info >= (3, 9): + def was_timeout_cancelled(): return _SENTINEL in exc_val.args + else: + def was_timeout_cancelled(): return self._state == _State.TIMEOUT + if exc_type is asyncio.CancelledError and was_timeout_cancelled(): self._timeout_handler = None raise asyncio.TimeoutError From cbfcfd214bbcd942ffa96e8ba698d1f961b205ef Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Fri, 3 Sep 2021 20:54:10 +0100 Subject: [PATCH 5/5] Annotations --- async_timeout/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/async_timeout/__init__.py b/async_timeout/__init__.py index 8ae90ab..514b05d 100644 --- a/async_timeout/__init__.py +++ b/async_timeout/__init__.py @@ -194,12 +194,12 @@ def _do_enter(self) -> None: def _do_exit(self, exc_type: Type[BaseException], exc_val: BaseException) -> None: if sys.version_info >= (3, 9): - def was_timeout_cancelled(): + def was_timeout_cancelled() -> bool: return _SENTINEL in exc_val.args else: - def was_timeout_cancelled(): + def was_timeout_cancelled() -> bool: return self._state == _State.TIMEOUT if exc_type is asyncio.CancelledError and was_timeout_cancelled():