Skip to content

Commit

Permalink
Merge branch 'main' into wait-random-exponential-min
Browse files Browse the repository at this point in the history
  • Loading branch information
yxtay authored Mar 2, 2024
2 parents 09aefef + c5d2d8b commit 5a28ca7
Show file tree
Hide file tree
Showing 9 changed files with 237 additions and 78 deletions.
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'monthly'
groups:
github-actions:
patterns:
- '*'
5 changes: 2 additions & 3 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ name: Continuous Integration
permissions: read-all

on:
push:
pull_request:
branches:
- main
Expand Down Expand Up @@ -35,12 +34,12 @@ jobs:
tox: mypy
steps:
- name: Checkout 🛎️
uses: actions/checkout@v4.0.0
uses: actions/checkout@v4.1.1
with:
fetch-depth: 0

- name: Setup Python 🔧
uses: actions/setup-python@v4.7.0
uses: actions/setup-python@v5.0.0
with:
python-version: ${{ matrix.python }}
allow-prereleases: true
Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
name: Release deploy

on:
push:
tags:
release:
types:
- published

jobs:
test:
if: github.repository_owner == 'jd'
publish:
timeout-minutes: 20
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v4.0.0
uses: actions/checkout@v4.1.1
with:
fetch-depth: 0

- name: Setup Python 🔧
uses: actions/setup-python@v4.7.0
uses: actions/setup-python@v5.0.0
with:
python-version: 3.11

Expand Down
49 changes: 14 additions & 35 deletions .mergify.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
queue_rules:
- name: default
conditions: &CheckRuns
merge_method: squash
queue_conditions:
- or:
- author = jd
- "#approved-reviews-by >= 1"
- author = dependabot[bot]
- or:
- files ~= ^releasenotes/notes/
- label = no-changelog
- author = dependabot[bot]
- "check-success=test (3.8, py38)"
- "check-success=test (3.9, py39)"
- "check-success=test (3.10, py310)"
Expand All @@ -20,42 +29,12 @@ pull_request_rules:
⚠️ No release notes detected. Please make sure to use
[reno](https://docs.openstack.org/reno/latest/user/usage.html) to add
a changelog entry.
- name: automatic merge without changelog
conditions:
- and: *CheckRuns
- "#approved-reviews-by>=1"
- label=no-changelog
actions:
queue:
name: default
method: squash
- name: automatic merge with changelog
conditions:
- and: *CheckRuns
- "#approved-reviews-by>=1"
- files~=^releasenotes/notes/
actions:
queue:
name: default
method: squash
- name: automatic merge for jd without changelog
conditions:
- author=jd
- and: *CheckRuns
- label=no-changelog
actions:
queue:
name: default
method: squash
- name: automatic merge for jd with changelog
conditions:
- author=jd
- and: *CheckRuns
- files~=^releasenotes/notes/
- name: automatic queue
conditions: []
actions:
queue:
name: default
method: squash

- name: dismiss reviews
conditions: []
actions:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
other:
- |
Add a Dependabot configuration submit PRs monthly (as needed)
to keep GitHub action versions updated.
132 changes: 102 additions & 30 deletions tenacity/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


import dataclasses
import functools
import sys
import threading
Expand Down Expand Up @@ -97,6 +96,29 @@
WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Any])


dataclass_kwargs = {}
if sys.version_info >= (3, 10):
dataclass_kwargs.update({"slots": True})


@dataclasses.dataclass(**dataclass_kwargs)
class IterState:
actions: t.List[t.Callable[["RetryCallState"], t.Any]] = dataclasses.field(
default_factory=list
)
retry_run_result: bool = False
delay_since_first_attempt: int = 0
stop_run_result: bool = False
is_explicit_retry: bool = False

def reset(self) -> None:
self.actions = []
self.retry_run_result = False
self.delay_since_first_attempt = 0
self.stop_run_result = False
self.is_explicit_retry = False


class TryAgain(Exception):
"""Always retry the executed function when raised."""

Expand Down Expand Up @@ -287,6 +309,14 @@ def statistics(self) -> t.Dict[str, t.Any]:
self._local.statistics = t.cast(t.Dict[str, t.Any], {})
return self._local.statistics

@property
def iter_state(self) -> IterState:
try:
return self._local.iter_state # type: ignore[no-any-return]
except AttributeError:
self._local.iter_state = IterState()
return self._local.iter_state

def wraps(self, f: WrappedFn) -> WrappedFn:
"""Wrap a function for retrying.
Expand All @@ -313,45 +343,89 @@ def begin(self) -> None:
self.statistics["attempt_number"] = 1
self.statistics["idle_for"] = 0

def iter(self, retry_state: "RetryCallState") -> t.Union[DoAttempt, DoSleep, t.Any]: # noqa
fut = retry_state.outcome
if fut is None:
if self.before is not None:
self.before(retry_state)
return DoAttempt()

is_explicit_retry = fut.failed and isinstance(fut.exception(), TryAgain)
if not (is_explicit_retry or self.retry(retry_state)):
return fut.result()
def _add_action_func(self, fn: t.Callable[..., t.Any]) -> None:
self.iter_state.actions.append(fn)

if self.after is not None:
self.after(retry_state)
def _run_retry(self, retry_state: "RetryCallState") -> None:
self.iter_state.retry_run_result = self.retry(retry_state)

def _run_wait(self, retry_state: "RetryCallState") -> None:
if self.wait:
sleep = self.wait(retry_state)
else:
sleep = 0.0

retry_state.upcoming_sleep = sleep

def _run_stop(self, retry_state: "RetryCallState") -> None:
self.statistics["delay_since_first_attempt"] = retry_state.seconds_since_start
if self.stop(retry_state):
self.iter_state.stop_run_result = self.stop(retry_state)

def iter(self, retry_state: "RetryCallState") -> t.Union[DoAttempt, DoSleep, t.Any]: # noqa
self._begin_iter(retry_state)
result = None
for action in self.iter_state.actions:
result = action(retry_state)
return result

def _begin_iter(self, retry_state: "RetryCallState") -> None: # noqa
self.iter_state.reset()

fut = retry_state.outcome
if fut is None:
if self.before is not None:
self._add_action_func(self.before)
self._add_action_func(lambda rs: DoAttempt())
return

self.iter_state.is_explicit_retry = fut.failed and isinstance(
fut.exception(), TryAgain
)
if not self.iter_state.is_explicit_retry:
self._add_action_func(self._run_retry)
self._add_action_func(self._post_retry_check_actions)

def _post_retry_check_actions(self, retry_state: "RetryCallState") -> None:
if not (self.iter_state.is_explicit_retry or self.iter_state.retry_run_result):
self._add_action_func(lambda rs: rs.outcome.result())
return

if self.after is not None:
self._add_action_func(self.after)

self._add_action_func(self._run_wait)
self._add_action_func(self._run_stop)
self._add_action_func(self._post_stop_check_actions)

def _post_stop_check_actions(self, retry_state: "RetryCallState") -> None:
if self.iter_state.stop_run_result:
if self.retry_error_callback:
return self.retry_error_callback(retry_state)
retry_exc = self.retry_error_cls(fut)
if self.reraise:
raise retry_exc.reraise()
raise retry_exc from fut.exception()
self._add_action_func(self.retry_error_callback)
return

def exc_check(rs: "RetryCallState") -> None:
fut = t.cast(Future, rs.outcome)
retry_exc = self.retry_error_cls(fut)
if self.reraise:
raise retry_exc.reraise()
raise retry_exc from fut.exception()

self._add_action_func(exc_check)
return

def next_action(rs: "RetryCallState") -> None:
sleep = rs.upcoming_sleep
rs.next_action = RetryAction(sleep)
rs.idle_for += sleep
self.statistics["idle_for"] += sleep
self.statistics["attempt_number"] += 1

retry_state.next_action = RetryAction(sleep)
retry_state.idle_for += sleep
self.statistics["idle_for"] += sleep
self.statistics["attempt_number"] += 1
self._add_action_func(next_action)

if self.before_sleep is not None:
self.before_sleep(retry_state)
self._add_action_func(self.before_sleep)

return DoSleep(sleep)
self._add_action_func(lambda rs: DoSleep(rs.upcoming_sleep))

def __iter__(self) -> t.Generator[AttemptManager, None, None]:
self.begin()
Expand Down Expand Up @@ -514,8 +588,7 @@ def __repr__(self) -> str:


@t.overload
def retry(func: WrappedFn) -> WrappedFn:
...
def retry(func: WrappedFn) -> WrappedFn: ...


@t.overload
Expand All @@ -530,8 +603,7 @@ def retry(
reraise: bool = False,
retry_error_cls: t.Type["RetryError"] = RetryError,
retry_error_callback: t.Optional[t.Callable[["RetryCallState"], t.Any]] = None,
) -> t.Callable[[WrappedFn], WrappedFn]:
...
) -> t.Callable[[WrappedFn], WrappedFn]: ...


def retry(*dargs: t.Any, **dkw: t.Any) -> t.Any:
Expand Down
46 changes: 44 additions & 2 deletions tenacity/_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from tenacity import DoAttempt
from tenacity import DoSleep
from tenacity import RetryCallState
from tenacity import _utils

WrappedFnReturnT = t.TypeVar("WrappedFnReturnT")
WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Awaitable[t.Any]])
Expand All @@ -46,7 +47,7 @@ async def __call__( # type: ignore[override]

retry_state = RetryCallState(retry_object=self, fn=fn, args=args, kwargs=kwargs)
while True:
do = self.iter(retry_state=retry_state)
do = await self.iter(retry_state=retry_state)
if isinstance(do, DoAttempt):
try:
result = await fn(*args, **kwargs)
Expand All @@ -60,6 +61,47 @@ async def __call__( # type: ignore[override]
else:
return do # type: ignore[no-any-return]

@classmethod
def _wrap_action_func(cls, fn: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
if _utils.is_coroutine_callable(fn):
return fn

async def inner(*args: t.Any, **kwargs: t.Any) -> t.Any:
return fn(*args, **kwargs)

return inner

def _add_action_func(self, fn: t.Callable[..., t.Any]) -> None:
self.iter_state.actions.append(self._wrap_action_func(fn))

async def _run_retry(self, retry_state: "RetryCallState") -> None: # type: ignore[override]
self.iter_state.retry_run_result = await self._wrap_action_func(self.retry)(
retry_state
)

async def _run_wait(self, retry_state: "RetryCallState") -> None: # type: ignore[override]
if self.wait:
sleep = await self._wrap_action_func(self.wait)(retry_state)
else:
sleep = 0.0

retry_state.upcoming_sleep = sleep

async def _run_stop(self, retry_state: "RetryCallState") -> None: # type: ignore[override]
self.statistics["delay_since_first_attempt"] = retry_state.seconds_since_start
self.iter_state.stop_run_result = await self._wrap_action_func(self.stop)(
retry_state
)

async def iter(
self, retry_state: "RetryCallState"
) -> t.Union[DoAttempt, DoSleep, t.Any]: # noqa: A003
self._begin_iter(retry_state)
result = None
for action in self.iter_state.actions:
result = await action(retry_state)
return result

def __iter__(self) -> t.Generator[AttemptManager, None, None]:
raise TypeError("AsyncRetrying object is not iterable")

Expand All @@ -70,7 +112,7 @@ def __aiter__(self) -> "AsyncRetrying":

async def __anext__(self) -> AttemptManager:
while True:
do = self.iter(retry_state=self._retry_state)
do = await self.iter(retry_state=self._retry_state)
if do is None:
raise StopAsyncIteration
elif isinstance(do, DoAttempt):
Expand Down
Loading

0 comments on commit 5a28ca7

Please sign in to comment.