Skip to content
Open
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
7 changes: 6 additions & 1 deletion tenacity/retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,12 @@ def _check(self, exception: BaseException) -> bool:
if self.message:
return self.message == str(exception)
assert self.match is not None
return bool(self.match.match(str(exception)))
# Use re.search (not re.match) so the pattern is found anywhere in
# the exception message. re.match only matches at the start, so a
# user-supplied pattern like r"rate limit" would never match
# "HTTP 429: rate limit hit". The parameter is named ``match``,
# meaning "find a match in the message", not "match from offset 0".
return bool(self.match.search(str(exception)))


class retry_if_not_exception_message(retry_if_exception_message):
Expand Down
40 changes: 39 additions & 1 deletion tests/test_tenacity.py
Original file line number Diff line number Diff line change
Expand Up @@ -1173,10 +1173,10 @@ def _retryable_test_with_unless_exception_type_no_input(


@retry(
stop=tenacity.stop_after_attempt(5),
retry=tenacity.retry_if_exception_message(
message=NoCustomErrorAfterCount.derived_message
),
stop=tenacity.stop_after_attempt(5),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to change that

)
def _retryable_test_if_exception_message_message(thing: typing.Any) -> typing.Any:
return thing.go()
Expand Down Expand Up @@ -1209,6 +1209,14 @@ def _retryable_test_if_not_exception_message_match(thing: typing.Any) -> typing.
return thing.go()


@retry(
retry=tenacity.retry_if_exception_message(match="limit exceeded"),
stop=tenacity.stop_after_attempt(5),
)
def _retryable_test_if_exception_message_match_search(thing: typing.Any) -> typing.Any:
return thing.go()


@retry(
retry=tenacity.retry_if_not_exception_message(
message=NameErrorUntilCount.derived_message
Expand Down Expand Up @@ -1378,6 +1386,36 @@ def test_retry_if_exception_message_match(self) -> None:
except CustomError:
self.fail("CustomError should've been retried from errormessage")

def test_retry_if_exception_message_match_finds_pattern_anywhere(self) -> None:
# Regression: the `match` kwarg used to call re.Pattern.match(), which
# only matches at the start of the message. A user who wrote
# `match="limit exceeded"` to catch `"HTTP 429: rate limit exceeded"`
# got zero retries because the pattern was not anchored at index 0.
# The fix uses re.Pattern.search() so the pattern is found anywhere
# in the message, which is what the parameter name implies.
class MessageCount:
derived_message = "HTTP 429: rate limit exceeded"

def __init__(self, count: int) -> None:
self.counter = 0
self.count = count

def go(self) -> bool:
if self.counter < self.count:
self.counter += 1
raise CustomError(self.derived_message)
return True

try:
self.assertTrue(
_retryable_test_if_exception_message_match_search(MessageCount(3))
)
except CustomError:
self.fail(
"CustomError should've been retried: the 'limit exceeded' pattern "
"appears in the message and should trigger a retry"
)

def test_retry_if_not_exception_message_match(self) -> None:
try:
self.assertTrue(
Expand Down
Loading