From 261bda40a5c2cf804fc40fff7838cc33a8b1b64e Mon Sep 17 00:00:00 2001 From: Jordan Cook Date: Tue, 15 Feb 2022 12:47:04 -0600 Subject: [PATCH 1/3] Fix disabling expiration for a single request with `CachedSession.request(..., expire_after=-1)` --- requests_cache/cache_control.py | 16 ++++++++++------ tests/unit/test_session.py | 13 +++++++++++-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/requests_cache/cache_control.py b/requests_cache/cache_control.py index d7d6f2ca..81fbcace 100644 --- a/requests_cache/cache_control.py +++ b/requests_cache/cache_control.py @@ -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'] @@ -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 @@ -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: @@ -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: @@ -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: diff --git a/tests/unit/test_session.py b/tests/unit/test_session.py index 9fc5df0d..09b491fe 100644 --- a/tests/unit/test_session.py +++ b/tests/unit/test_session.py @@ -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 @@ -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 From 1f5d6f6ff180e4755e8ac1a69b646a33e2259b23 Mon Sep 17 00:00:00 2001 From: Jordan Cook Date: Tue, 15 Feb 2022 12:47:40 -0600 Subject: [PATCH 2/3] Use only integers for expire_after values in tests and docs --- docs/user_guide/expiration.md | 6 +++--- tests/unit/test_session.py | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/user_guide/expiration.md b/docs/user_guide/expiration.md index 37bc010c..3113604d 100644 --- a/docs/user_guide/expiration.md +++ b/docs/user_guide/expiration.md @@ -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 diff --git a/tests/unit/test_session.py b/tests/unit/test_session.py index 09b491fe..626231d8 100644 --- a/tests/unit/test_session.py +++ b/tests/unit/test_session.py @@ -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 @@ -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) @@ -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): @@ -528,12 +528,12 @@ 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 @@ -541,12 +541,12 @@ def test_stale_if_error__exception(mock_session): 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 From 7f64c17aa074628678515d21cf9b18fc86c4c197 Mon Sep 17 00:00:00 2001 From: Jordan Cook Date: Tue, 15 Feb 2022 13:00:37 -0600 Subject: [PATCH 3/3] Update changelog and contributors --- .all-contributorsrc | 9 +++++++++ CONTRIBUTORS.md | 13 +++++++------ HISTORY.md | 7 ++++--- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 06900bf5..d8833825 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -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, diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index b53fba89..a53abae4 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -47,58 +47,59 @@ contributions that have helped to improve requests-cache:
Jessy Williams

πŸ’» πŸ› ⚠️
John Vandenberg

πŸš‡ πŸ“¦ ⚠️ +
Jonathan Liuti

πŸ›
JonΓ‘Ε‘ JančaΕ™Γ­k

πŸ›
Jordan Cook

πŸ’» 🚧 ✨ πŸ› ⚠️ πŸ“– πŸš‡
JΓΌrgen Hermann

πŸ› πŸ€”
Kieran W

πŸ“– πŸ› -
MHellmund

πŸ› +
MHellmund

πŸ›
Manuel Eggimann

πŸ› πŸ’»
Marc Abramowitz

πŸ’» πŸ“–
Marius Gedminas

πŸ’» πŸ›
Masahiro Wada

πŸ’» ✨
Massimo Santini

πŸ€”
Michael Herman

πŸ’» πŸ“– -
MichaΕ‚ GΓ³rny

πŸš‡ +
MichaΕ‚ GΓ³rny

πŸš‡
MichaΕ‚ Nowotka

πŸ€”
Mike

πŸ’» ✨
Nate

πŸ›
Nathan Cahill

πŸ›
Nick

πŸ€”
Olivier Dalang

πŸ’» -
Parker Hancock

πŸ’» ✨ πŸ› ⚠️ πŸ“– πŸ›‘οΈ πŸ€” +
Parker Hancock

πŸ’» ✨ πŸ› ⚠️ πŸ“– πŸ›‘οΈ πŸ€”
Philipp A.

πŸ›
Roderic Day

πŸ›
Roman Haritonov

πŸ’» 🚧 ✨ πŸ› ⚠️ πŸ“– πŸš‡
Samuel T.

πŸ› πŸ€”
Sebastian HΓΆffner

πŸ’» ✨ ⚠️ πŸ€”
Serhii Chvaliuk

πŸ› πŸ’» -
Simon Biewald

πŸ›‘οΈ πŸ€” +
Simon Biewald

πŸ›‘οΈ πŸ€”
Skipper Seabold

πŸ›
Slin Lee

πŸ“–
Stavros Korokithakis

πŸš‡ πŸ”§ πŸ“–
Taher Chegini

πŸ›
Vladimir Panteleev

πŸ€”
Willem de Groot

πŸ’» πŸ› -
Wouter Vanden Hove

πŸ› +
Wouter Vanden Hove

πŸ›
YetAnotherNerd

πŸ’» ✨ πŸ›
aaron-mf1

πŸ€”
coryairbhb

πŸ›
craig

πŸ’» πŸ›
denis-bz

πŸ›
girst

πŸ› -
gorogoroumaru

πŸ’» +
gorogoroumaru

πŸ’»
harvey251

πŸ›
mbarkhau

πŸ’» ⚠️ πŸš‡ πŸ›
shiftinv

πŸ’» πŸ› diff --git a/HISTORY.md b/HISTORY.md index b3e9a0c4..1d80b308 100644 --- a/HISTORY.md +++ b/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)