Skip to content

Commit

Permalink
Merge pull request #527 from JWCook/fix-expire-override
Browse files Browse the repository at this point in the history
Fix disabling expiration for a single request
  • Loading branch information
JWCook committed Feb 15, 2022
2 parents 04ff30b + 7f64c17 commit c2baccd
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 30 deletions.
9 changes: 9 additions & 0 deletions .all-contributorsrc
Expand Up @@ -744,6 +744,15 @@
"contributions": [
"bug"
]
},
{
"login": "johnraz",
"name": "Jonathan Liuti",
"avatar_url": "https://avatars.githubusercontent.com/u/304164?v=4",
"profile": "https://github.com/johnraz",
"contributions": [
"bug"
]
}
],
"contributorsPerLine": 7,
Expand Down
13 changes: 7 additions & 6 deletions CONTRIBUTORS.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions HISTORY.md
@@ -1,8 +1,9 @@
# History

## 0.9.2 (Unreleased)
* Support `params`, `data`, and `json` as positional arguments to `CachedSession.request()` (fixes
regression in `0.9.0`)
## 0.9.2 (2022-02-15)
Fix some regression bugs introduced in 0.9.0:
* Add support `params` as a positional argument to `CachedSession.request()`
* Add support for disabling expiration for a single request with `CachedSession.request(..., expire_after=-1)`

## 0.9.1 (2022-01-15)
* Add support for python 3.10.2 and 3.9.10 (regarding resolving `ForwardRef` types during deserialization)
Expand Down
6 changes: 3 additions & 3 deletions docs/user_guide/expiration.md
Expand Up @@ -75,10 +75,10 @@ retrieving a new response. If you would like to use expired response data in the

For example:
```python
>>> # Cache a test response that will expire immediately
>>> # Cache a test response and wait until it's expired
>>> session = CachedSession(stale_if_error=True)
>>> session.get('https://httpbin.org/get', expire_after=0.0001)
>>> time.sleep(0.0001)
>>> session.get('https://httpbin.org/get', expire_after=1)
>>> time.sleep(1)
```

Afterward, let's say the page has moved and you get a 404, or the site is experiencing downtime and
Expand Down
16 changes: 10 additions & 6 deletions requests_cache/cache_control.py
Expand Up @@ -27,8 +27,9 @@

__all__ = ['DO_NOT_CACHE', 'CacheActions']

# May be set by either headers or expire_after param to disable caching
# May be set by either headers or expire_after param to disable caching or disable expiration
DO_NOT_CACHE = 0
NEVER_EXPIRE = -1
# Supported Cache-Control directives
CACHE_DIRECTIVES = ['immutable', 'max-age', 'no-cache', 'no-store']

Expand Down Expand Up @@ -139,7 +140,7 @@ def update_from_response(self, response: Response):

# Check headers for expiration, validators, and other cache directives
if directives.get('immutable'):
self.expire_after = -1
self.expire_after = NEVER_EXPIRE
else:
self.expire_after = coalesce(
directives.get('max-age'), directives.get('expires'), self.expire_after
Expand All @@ -156,7 +157,7 @@ def update_from_response(self, response: Response):
def get_expiration_datetime(expire_after: ExpirationTime) -> Optional[datetime]:
"""Convert an expiration value in any supported format to an absolute datetime"""
# Never expire
if expire_after is None or expire_after == -1:
if expire_after is None or expire_after == NEVER_EXPIRE:
return None
# Expire immediately
elif try_int(expire_after) == DO_NOT_CACHE:
Expand All @@ -173,10 +174,10 @@ def get_expiration_datetime(expire_after: ExpirationTime) -> Optional[datetime]:
return datetime.utcnow() + expire_after


def get_expiration_seconds(expire_after: ExpirationTime) -> Optional[int]:
def get_expiration_seconds(expire_after: ExpirationTime) -> int:
"""Convert an expiration value in any supported format to an expiration time in seconds"""
expires = get_expiration_datetime(expire_after)
return ceil((expires - datetime.utcnow()).total_seconds()) if expires else None
return ceil((expires - datetime.utcnow()).total_seconds()) if expires else NEVER_EXPIRE


def get_cache_directives(headers: Mapping) -> Dict:
Expand Down Expand Up @@ -242,7 +243,10 @@ def to_utc(dt: datetime):

def try_int(value: Any) -> Optional[int]:
"""Convert a value to an int, if possible, otherwise ``None``"""
return int(str(value)) if str(value).isnumeric() else None
try:
return int(value)
except (TypeError, ValueError):
return None


def url_match(url: str, pattern: str) -> bool:
Expand Down
33 changes: 21 additions & 12 deletions tests/unit/test_session.py
Expand Up @@ -10,7 +10,7 @@

import pytest
import requests
from requests import Request
from requests import Request, RequestException
from requests.structures import CaseInsensitiveDict

from requests_cache import ALL_METHODS, CachedResponse, CachedSession
Expand Down Expand Up @@ -154,12 +154,12 @@ def test_response_history(mock_session):

def test_repr(mock_session):
"""Test session and cache string representations"""
mock_session.expire_after = 10.5
mock_session.expire_after = 11
mock_session.cache.responses['key'] = 'value'
mock_session.cache.redirects['key'] = 'value'
mock_session.cache.redirects['key_2'] = 'value'

assert mock_session.cache.cache_name in repr(mock_session) and '10.5' in repr(mock_session)
assert mock_session.cache.cache_name in repr(mock_session) and '11' in repr(mock_session)
assert '2 redirects' in str(mock_session.cache) and '1 responses' in str(mock_session.cache)


Expand Down Expand Up @@ -515,9 +515,9 @@ def test_expired_request_error(mock_session):
"""Without stale_if_error (default), if there is an error while re-fetching an expired
response, the request should be re-raised and the expired item deleted"""
mock_session.stale_if_error = False
mock_session.expire_after = 0.01
mock_session.expire_after = 1
mock_session.get(MOCKED_URL)
time.sleep(0.01)
time.sleep(1)

with patch.object(mock_session.cache, 'save_response', side_effect=ValueError):
with pytest.raises(ValueError):
Expand All @@ -528,25 +528,25 @@ def test_expired_request_error(mock_session):
def test_stale_if_error__exception(mock_session):
"""With stale_if_error, expect to get old cache data if there is an exception during a request"""
mock_session.stale_if_error = True
mock_session.expire_after = 0.2
mock_session.expire_after = 1

assert mock_session.get(MOCKED_URL).from_cache is False
assert mock_session.get(MOCKED_URL).from_cache is True
time.sleep(0.2)
with patch.object(mock_session.cache, 'save_response', side_effect=ValueError):
time.sleep(1)
with patch.object(mock_session.cache, 'save_response', side_effect=RequestException):
response = mock_session.get(MOCKED_URL)
assert response.from_cache is True and response.is_expired is True


def test_stale_if_error__error_code(mock_session):
"""With stale_if_error, expect to get old cache data if a response has an error status code"""
mock_session.stale_if_error = True
mock_session.expire_after = 0.2
mock_session.expire_after = 1
mock_session.allowable_codes = (200, 404)

assert mock_session.get(MOCKED_URL_404).from_cache is False

time.sleep(0.2)
time.sleep(1)
response = mock_session.get(MOCKED_URL_404)
assert response.from_cache is True and response.is_expired is True

Expand Down Expand Up @@ -718,8 +718,8 @@ def test_remove_expired_responses__per_request(mock_session):
assert len(mock_session.cache.responses) == 1


def test_per_request__expiration(mock_session):
"""No per-session expiration is set, but then overridden with per-request expiration"""
def test_per_request__enable_expiration(mock_session):
"""No per-session expiration is set, but then overridden for a single request"""
mock_session.expire_after = None
response = mock_session.get(MOCKED_URL, expire_after=1)
assert response.from_cache is False
Expand All @@ -730,6 +730,15 @@ def test_per_request__expiration(mock_session):
assert response.from_cache is False


def test_per_request__disable_expiration(mock_session):
"""A per-session expiration is set, but then disabled for a single request"""
mock_session.expire_after = 60
response = mock_session.get(MOCKED_URL, expire_after=-1)
response = mock_session.get(MOCKED_URL, expire_after=-1)
assert response.from_cache is True
assert response.expires is None


def test_per_request__prepared_request(mock_session):
"""The same should work for PreparedRequests with CachedSession.send()"""
mock_session.expire_after = None
Expand Down

0 comments on commit c2baccd

Please sign in to comment.