Skip to content

Commit

Permalink
Merge 1cabac1 into b2db35f
Browse files Browse the repository at this point in the history
  • Loading branch information
LilSpazJoekp committed Nov 1, 2021
2 parents b2db35f + 1cabac1 commit 286524e
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 41 deletions.
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Unreleased
submission has already been fetched and a ``warn_comment_sort`` config setting to turn
off the warning.
- :meth:`.user_selectable` to get available subreddit link flairs.
- Automatic RateLimit handling will support errors with millisecond resolution.

**Fixed**

Expand Down
39 changes: 21 additions & 18 deletions asyncpraw/reddit.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class Reddit:
"""

update_checked = False
_ratelimit_regex = re.compile(r"([0-9]{1,2}) (seconds?|minutes?)")
_ratelimit_regex = re.compile(r"([0-9]{1,3}) (milliseconds?|seconds?|minutes?)")

@property
def _next_unique(self) -> int:
Expand Down Expand Up @@ -747,10 +747,12 @@ def _handle_rate_limit(
if not amount_search:
break
seconds = int(amount_search.group(1))
if "minute" in amount_search.group(2):
if amount_search.group(2).startswith("minute"):
seconds *= 60
elif amount_search.group(2).startswith("millisecond"):
seconds = 0
if seconds <= int(self.config.ratelimit_seconds):
sleep_seconds = seconds + min(seconds / 10, 1)
sleep_seconds = seconds + 1
return sleep_seconds
return None

Expand Down Expand Up @@ -817,28 +819,29 @@ async def post(
"""
if json is None:
data = data or {}
try:
return await self._objectify_request(
data=data,
files=files,
json=json,
method="POST",
params=params,
path=path,
)
except RedditAPIException as exception:
seconds = self._handle_rate_limit(exception=exception)
if seconds is not None:
logger.debug(f"Rate limit hit, sleeping for {seconds:.2f} seconds")
await asyncio.sleep(seconds)

attempts = 3
last_exception = None
while attempts > 0:
attempts -= 1
try:
return await self._objectify_request(
data=data,
files=files,
json=json,
method="POST",
params=params,
path=path,
)
raise
except RedditAPIException as exception:
last_exception = exception
seconds = self._handle_rate_limit(exception=exception)
if seconds is None:
break
second_string = "second" if seconds == 1 else "seconds"
logger.debug(f"Rate limit hit, sleeping for {seconds} {second_string}")
await asyncio.sleep(seconds)
raise last_exception

async def put(
self,
Expand Down
10 changes: 4 additions & 6 deletions docs/getting_started/configuration/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,13 @@ Miscellaneous Configuration Options
These are options that do not belong in another category, but still play a part in Async
PRAW.

:ratelimit_seconds: Controls the maximum amount of seconds Async PRAW will capture
ratelimits returned in JSON data. Because this can be as high as 10 minutes, only
ratelimits of up to 5 seconds are captured and waited on by default. Should be a
number representing the amount of seconds to sleep.
:ratelimit_seconds: Controls the maximum number of seconds Async PRAW will capture
ratelimits returned in JSON data. Because this can be as high as 14 minutes, only
ratelimits of up to 5 seconds are captured and waited on by default.

.. note::

Async PRAW sleeps for the ratelimit plus either 1/10th of the ratelimit or 1
second, whichever is smallest.
Async PRAW sleeps for the ratelimit value plus 1 second.

:timeout: Controls the amount of time Async PRAW will wait for a request from Reddit to
complete before throwing an exception. By default, Async PRAW waits 16 seconds
Expand Down
11 changes: 11 additions & 0 deletions prepare-commit-msg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env python
import re
import sys

commit_msg_filepath = sys.argv[1]
if __name__ == "__main__":
regex = re.compile(r"(\(cherry picked from commit )([a-h0-9]*)")
with open(commit_msg_filepath, "r+") as f:
commit_msg = f.read()
f.seek(0, 0)
f.write(regex.sub(r"\1praw-dev/praw@\2", commit_msg))
167 changes: 150 additions & 17 deletions tests/unit/test_reddit.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,136 @@ async def test_multireddit(self):
multireddit = await self.reddit.multireddit("bboe", "aa")
assert multireddit.path == "/user/bboe/m/aa"

@mock.patch(
"asyncpraw.Reddit.request",
side_effect=[
{
"json": {
"errors": [
[
"RATELIMIT",
"Some unexpected error message",
"ratelimit",
]
]
}
},
],
)
@mock.patch("asyncio.sleep", return_value=None)
async def test_post_ratelimit__invalid_rate_limit_message(self, mock_sleep, _):
with pytest.raises(RedditAPIException) as exception:
await self.reddit.post("test")
assert exception.value.message == "Some unexpected error message"
mock_sleep.assert_not_called()

@mock.patch(
"asyncpraw.Reddit.request",
side_effect=[
{
"json": {
"errors": [
[
"RATELIMIT",
"You are doing that too much. Try again in 6 seconds.",
"ratelimit",
]
]
}
},
],
)
@mock.patch("asyncio.sleep", return_value=None)
async def test_post_ratelimit__over_threshold__seconds(self, mock_sleep, _):
with pytest.raises(RedditAPIException) as exception:
await self.reddit.post("test")
assert (
exception.value.message
== "You are doing that too much. Try again in 6 seconds."
)
mock_sleep.assert_not_called()

@mock.patch(
"asyncpraw.Reddit.request",
side_effect=[
{
"json": {
"errors": [
[
"RATELIMIT",
"You are doing that too much. Try again in 1 minute.",
"ratelimit",
]
]
}
},
],
)
@mock.patch("asyncio.sleep", return_value=None)
async def test_post_ratelimit__over_threshold__minutes(self, __, _):
with pytest.raises(RedditAPIException) as exception:
await self.reddit.post("test")
assert (
exception.value.message
== "You are doing that too much. Try again in 1 minute."
)

@mock.patch(
"asyncpraw.Reddit.request",
side_effect=[
{
"json": {
"errors": [
[
"RATELIMIT",
"You are doing that too much. Try again in 2 milliseconds.",
"ratelimit",
]
]
}
},
{
"json": {
"errors": [
[
"RATELIMIT",
"You are doing that too much. Try again in 1 millisecond.",
"ratelimit",
]
]
}
},
{},
],
)
@mock.patch("asyncio.sleep", return_value=None)
async def test_post_ratelimit__under_threshold__milliseconds(self, mock_sleep, _):
await self.reddit.post("test")
mock_sleep.assert_has_calls([mock.call(1), mock.call(1)])

@mock.patch(
"asyncpraw.Reddit.request",
side_effect=[
{
"json": {
"errors": [
[
"RATELIMIT",
"You are doing that too much. Try again in 1 minute.",
"ratelimit",
]
]
}
},
{},
],
)
@mock.patch("asyncio.sleep", return_value=None)
async def test_post_ratelimit__under_threshold__minutes(self, mock_sleep, _):
self.reddit.config.ratelimit_seconds = 60
await self.reddit.post("test")
mock_sleep.assert_has_calls([mock.call(61)])

@mock.patch(
"asyncpraw.Reddit.request",
side_effect=[
Expand All @@ -132,6 +262,17 @@ async def test_multireddit(self):
]
}
},
{},
],
)
@mock.patch("asyncio.sleep", return_value=None)
async def test_post_ratelimit__under_threshold__seconds(self, mock_sleep, _):
await self.reddit.post("test")
mock_sleep.assert_has_calls([mock.call(6)])

@mock.patch(
"asyncpraw.Reddit.request",
side_effect=[
{
"json": {
"errors": [
Expand All @@ -148,7 +289,7 @@ async def test_multireddit(self):
"errors": [
[
"RATELIMIT",
"You are doing that too much. Try again in 10 minutes.",
"You are doing that too much. Try again in 3 seconds.",
"ratelimit",
]
]
Expand All @@ -159,33 +300,25 @@ async def test_multireddit(self):
"errors": [
[
"RATELIMIT",
"APRIL FOOLS FROM REDDIT, TRY AGAIN",
"You are doing that too much. Try again in 1 second.",
"ratelimit",
]
]
}
},
{},
],
)
@mock.patch("asyncio.sleep", return_value=None)
async def test_post_ratelimit(self, __, _):
with pytest.raises(RedditAPIException) as exc:
await self.reddit.post("test")
assert (
exc.value.message == "You are doing that too much. Try again in 5 seconds."
)
with pytest.raises(RedditAPIException) as exc2:
async def test_post_ratelimit__under_threshold__seconds_failure(
self, mock_sleep, _
):
with pytest.raises(RedditAPIException) as exception:
await self.reddit.post("test")
assert (
exc2.value.message
== "You are doing that too much. Try again in 10 minutes."
exception.value.message
== "You are doing that too much. Try again in 1 second."
)
with pytest.raises(RedditAPIException) as exc3:
await self.reddit.post("test")
assert exc3.value.message == "APRIL FOOLS FROM REDDIT, TRY AGAIN"
response = await self.reddit.post("test")
assert response == {}
mock_sleep.assert_has_calls([mock.call(6), mock.call(4), mock.call(2)])

async def test_read_only__with_authenticated_core(self):
async with Reddit(
Expand Down

0 comments on commit 286524e

Please sign in to comment.