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
6 changes: 4 additions & 2 deletions scrapling/engines/static.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,8 @@ def _make_request(self, method: SUPPORTED_HTTP_METHODS, stealth: Optional[bool]
stealth = self._stealth if stealth is None else stealth

selector_config = self._get_param(kwargs, "selector_config", self.selector_config) or self.selector_config
max_retries = self._get_param(kwargs, "retries", self._default_retries)
# Always attempt the request once; `retries` below 1 (or `None`) means "send it, but don't retry"
max_retries = max(1, self._get_param(kwargs, "retries", self._default_retries) or 1)
retry_delay = self._get_param(kwargs, "retry_delay", self._default_retry_delay)
static_proxy = kwargs.pop("proxy", None)

Expand Down Expand Up @@ -443,7 +444,8 @@ async def _make_request(self, method: SUPPORTED_HTTP_METHODS, stealth: Optional[
stealth = self._stealth if stealth is None else stealth

selector_config = self._get_param(kwargs, "selector_config", self.selector_config) or self.selector_config
max_retries = self._get_param(kwargs, "retries", self._default_retries)
# Always attempt the request once; `retries` below 1 (or `None`) means "send it, but don't retry"
max_retries = max(1, self._get_param(kwargs, "retries", self._default_retries) or 1)
retry_delay = self._get_param(kwargs, "retry_delay", self._default_retry_delay)
static_proxy = kwargs.pop("proxy", None)

Expand Down
5 changes: 5 additions & 0 deletions tests/fetchers/async/test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,8 @@ async def test_selector_config_overrides_configure(
)
assert response._storage is not None
assert response.url == "from-request.test"

async def test_retries_below_one_still_performs_the_request(self, fetcher, urls):
"""``retries`` below 1 means "send the request once", not "send nothing"."""
assert (await fetcher.get(urls["status_200"], retries=0)).status == 200
assert (await fetcher.get(urls["status_200"], retries=-1)).status == 200
25 changes: 25 additions & 0 deletions tests/fetchers/async/test_requests_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,28 @@ async def test_proxy_rotates_per_retry_attempt(self):

proxies_used = [call.kwargs["proxy"] for call in mocked_request.call_args_list]
assert proxies_used == ["http://p1:8080", "http://p2:8080"]

@pytest.mark.asyncio
@pytest.mark.parametrize("retries", [0, -1, None])
async def test_retries_below_one_still_sends_the_request(self, retries):
"""A session-level retries below 1 must still send the request once instead of skipping it"""
async with AsyncFetcherSession(retries=retries, retry_delay=0) as session:
with (
patch.object(session._async_curl_session, "request", new=AsyncMock()) as mocked_request,
patch("scrapling.engines.static.ResponseFactory.from_http_request", return_value=MagicMock()),
):
await session.get("http://example.com")

assert mocked_request.call_count == 1

@pytest.mark.asyncio
async def test_per_request_retries_below_one_still_sends_the_request(self):
"""A per-request retries of 0 must override the session default without skipping the request"""
async with AsyncFetcherSession(retries=3, retry_delay=0) as session:
with (
patch.object(session._async_curl_session, "request", new=AsyncMock()) as mocked_request,
patch("scrapling.engines.static.ResponseFactory.from_http_request", return_value=MagicMock()),
):
await session.get("http://example.com", retries=0)

assert mocked_request.call_count == 1
5 changes: 5 additions & 0 deletions tests/fetchers/sync/test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,8 @@ def test_selector_config_overrides_configure(self, fetcher, _reset_fetcher_confi
)
assert response._storage is not None
assert response.url == "from-request.test"

def test_retries_below_one_still_performs_the_request(self, fetcher):
"""``retries`` below 1 means "send the request once", not "send nothing"."""
assert fetcher.get(self.status_200, retries=0).status == 200
assert fetcher.get(self.status_200, retries=-1).status == 200
23 changes: 23 additions & 0 deletions tests/fetchers/sync/test_requests_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,26 @@ def test_proxy_rotates_per_retry_attempt(self):

proxies_used = [call.kwargs["proxy"] for call in mocked_request.call_args_list]
assert proxies_used == ["http://p1:8080", "http://p2:8080"]

@pytest.mark.parametrize("retries", [0, -1, None])
def test_retries_below_one_still_sends_the_request(self, retries):
"""A session-level retries below 1 must still send the request once instead of skipping it"""
with FetcherSession(retries=retries, retry_delay=0) as session:
with (
patch.object(session._curl_session, "request") as mocked_request,
patch("scrapling.engines.static.ResponseFactory.from_http_request", return_value=MagicMock()),
):
session.get("http://example.com")

assert mocked_request.call_count == 1

def test_per_request_retries_below_one_still_sends_the_request(self):
"""A per-request retries of 0 must override the session default without skipping the request"""
with FetcherSession(retries=3, retry_delay=0) as session:
with (
patch.object(session._curl_session, "request") as mocked_request,
patch("scrapling.engines.static.ResponseFactory.from_http_request", return_value=MagicMock()),
):
session.get("http://example.com", retries=0)

assert mocked_request.call_count == 1