Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
fixes:
- |
Setting ``tenacity.tornado = None`` -- the usual way a downstream test
suite forces the non-tornado code path -- raised ``AttributeError: 'NoneType'
object has no attribute 'gen'`` from the ``@retry`` decorator. Tornado
availability is read live again rather than from an import-time snapshot.
15 changes: 9 additions & 6 deletions tenacity/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,13 @@
except ImportError:
tornado = None # type: ignore[assignment]

# mypy resolves `tornado` to the module (it only ever sees the `try` branch),
# so testing the module object for truthiness reads as an always-true check.
# Keep the availability answer in a plain bool instead.
_HAS_TORNADO = tornado is not None

def _has_tornado() -> bool:
# A function, not a module-level constant: test suites force the
# non-tornado path by setting `tenacity.tornado = None`, so the answer has
# to be computed from the live global every time it is asked for.
return tornado is not None


if t.TYPE_CHECKING:
if sys.version_info >= (3, 11):
Expand Down Expand Up @@ -786,7 +789,7 @@ def wrap(f: t.Callable[P, R]) -> _RetryDecorated[P, R]:
):
r = AsyncRetrying(*dargs, **dkw)
elif (
_HAS_TORNADO
_has_tornado()
and hasattr(tornado.gen, "is_coroutine_function")
and tornado.gen.is_coroutine_function(f)
):
Expand All @@ -801,7 +804,7 @@ def wrap(f: t.Callable[P, R]) -> _RetryDecorated[P, R]:

from tenacity.asyncio import AsyncRetrying # noqa: E402

if _HAS_TORNADO:
if _has_tornado():
from tenacity.tornadoweb import TornadoRetrying


Expand Down
13 changes: 13 additions & 0 deletions tests/test_tornado.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
import unittest
from collections.abc import Generator
from typing import Any
from unittest import mock

from tornado import gen, testing

import tenacity
from tenacity import RetryError, retry, stop_after_attempt, tornadoweb

from .test_tenacity import NoIOErrorAfterCount
Expand Down Expand Up @@ -73,6 +75,17 @@ def retryable(thing: NoIOErrorAfterCount) -> None:
finally:
gen.is_coroutine_function = old_attr

def test_tornado_set_to_none(self) -> None:
# Forcing the non-tornado path by nulling the module global is how
# downstream suites exercise installs without tornado.
with mock.patch.object(tenacity, "tornado", None):

@retry(stop=stop_after_attempt(1))
def retryable() -> int:
return 1

assert retryable() == 1


if __name__ == "__main__":
unittest.main()
Loading