Changed cache_handler.py to utilize Python's Context Management Protocol (#991)

This commit is contained in:
508chris 2025-01-22 18:36:37 -05:00 committed by GitHub
parent d319c6e09f
commit 1dbbbf65ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 121 additions and 64 deletions

View File

@ -4,7 +4,6 @@ on: [push, pull_request]
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
SPOTIPY_CLIENT_ID: ${{ secrets.SPOTIPY_CLIENT_ID }} SPOTIPY_CLIENT_ID: ${{ secrets.SPOTIPY_CLIENT_ID }}

View File

@ -4,7 +4,6 @@ on: [push, pull_request]
jobs: jobs:
build: build:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
strategy: strategy:
matrix: matrix:

View File

@ -6,22 +6,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased ## Unreleased
Add your changes below. Add your changes below.
### Added ### Added
- Added examples for audiobooks, shows and episodes methods to examples directory - Added examples for audiobooks, shows and episodes methods to examples directory
- Use newer string formatters (https://pyformat.info) - Use newer string formatters (https://pyformat.info)
- Marked `recommendation_genre_seeds` as deprecated - Marked `recommendation_genre_seeds` as deprecated
### Fixed ### Fixed
- Fixed scripts in examples directory that didn't run correctly - Fixed scripts in examples directory that didn't run correctly
- Updated documentation for `Client.current_user_top_artists` to indicate maximum number of artists limit - Updated documentation for `Client.current_user_top_artists` to indicate maximum number of artists limit
### Changed
- Updated get_cached_token and save_token_to_cache methods to utilize Python's Context Management Protocol
- Added except clause to get_cached_token method to handle json decode errors
### Removed ### Removed
## [2.25.0] - 2025-03-01 ## [2.25.0] - 2025-03-01
### Added ### Added
- Added unit tests for queue functions - Added unit tests for queue functions
- Added detailed function docstrings to 'util.py', including descriptions and special sections that lists arguments, returns, and raises. - Added detailed function docstrings to 'util.py', including descriptions and special sections that lists arguments, returns, and raises.
- Updated order of instructions for Python and pip package manager installation in TUTORIAL.md - Updated order of instructions for Python and pip package manager installation in TUTORIAL.md
@ -44,53 +53,66 @@ Add your changes below.
### Changed ### Changed
- Split test and lint workflows - Split test and lint workflows
- Updated get_cached_token and save_token_to_cache methods to utilize Python's Context Management Protocol
- Added except clause to get_cached_token method to handle json decode errors
### Fixed ### Fixed
- Audiobook integration tests - Audiobook integration tests
- Edited docstrings for certain functions in client.py for functions that are no longer in use and have been replaced. - Edited docstrings for certain functions in client.py for functions that are no longer in use and have been replaced.
- `current_user_unfollow_playlist()` now supports playlist IDs, URLs, and URIs rather than previously where it only supported playlist IDs. - `current_user_unfollow_playlist()` now supports playlist IDs, URLs, and URIs rather than previously where it only supported playlist IDs.
### Removed ### Removed
- `mock` no longer listed as a test dependency. Only built-in `unittest.mock` is actually used. - `mock` no longer listed as a test dependency. Only built-in `unittest.mock` is actually used.
## [2.24.0] - 2024-05-30 ## [2.24.0] - 2024-05-30
### Added ### Added
- Added `MemcacheCacheHandler`, a cache handler that stores the token info using pymemcache. - Added `MemcacheCacheHandler`, a cache handler that stores the token info using pymemcache.
- Added support for audiobook endpoints: `get_audiobook`, `get_audiobooks`, and `get_audiobook_chapters`. - Added support for audiobook endpoints: `get_audiobook`, `get_audiobooks`, and `get_audiobook_chapters`.
- Added integration tests for audiobook endpoints. - Added integration tests for audiobook endpoints.
- Added `update` field to `current_user_follow_playlist`. - Added `update` field to `current_user_follow_playlist`.
### Changed ### Changed
- Updated get_cached_token and save_token_to_cache methods to utilize Python's Context Management Protocol
- Added except clause to get_cached_token method to handle json decode errors
- Fixed error obfuscation when Spotify class is being inherited and an error is raised in the Child's `__init__` - Fixed error obfuscation when Spotify class is being inherited and an error is raised in the Child's `__init__`
- Replaced `artist_albums(album_type=...)` with `artist_albums(include_groups=...)` due to an API change. - Replaced `artist_albums(album_type=...)` with `artist_albums(include_groups=...)` due to an API change.
- Updated `_regex_spotify_url` to ignore `/intl-<countrycode>` in Spotify links - Updated `_regex_spotify_url` to ignore `/intl-<countrycode>` in Spotify links
- Improved README, docs and examples - Improved README, docs and examples
### Fixed ### Fixed
- Readthedocs build - Readthedocs build
- Split `test_current_user_save_and_usave_tracks` unit test - Split `test_current_user_save_and_usave_tracks` unit test
### Removed ### Removed
- Drop support for EOL Python 3.7 - Drop support for EOL Python 3.7
## [2.23.0] - 2023-04-07 ## [2.23.0] - 2023-04-07
### Added ### Added
- Added optional `encoder_cls` argument to `CacheFileHandler`, which overwrite default encoder for token before writing to disk - Added optional `encoder_cls` argument to `CacheFileHandler`, which overwrite default encoder for token before writing to disk
- Integration tests for searching multiple types in multiple markets (non-user endpoints) - Integration tests for searching multiple types in multiple markets (non-user endpoints)
- Publish to PyPI action - Publish to PyPI action
### Fixed ### Fixed
- Fixed the regex for matching playlist URIs with the format spotify:user:USERNAME:playlist:PLAYLISTID. - Fixed the regex for matching playlist URIs with the format spotify:user:USERNAME:playlist:PLAYLISTID.
- `search_markets` now factors the counts of all types in the `total` rather than just the first type ([#534](https://github.com/spotipy-dev/spotipy/issues/534)) - `search_markets` now factors the counts of all types in the `total` rather than just the first type ([#534](https://github.com/spotipy-dev/spotipy/issues/534))
## [2.22.1] - 2023-01-23 ## [2.22.1] - 2023-01-23
### Added ### Added
- Add alternative module installation instruction to README - Add alternative module installation instruction to README
- Added Comment to README - Getting Started for user to add URI to app in Spotify Developer Dashboard. - Added Comment to README - Getting Started for user to add URI to app in Spotify Developer Dashboard.
- Added playlist_add_tracks.py to example folder - Added playlist_add_tracks.py to example folder
### Changed ### Changed
@ -170,11 +192,11 @@ Add your changes below.
- Enabled using both short and long IDs for playlist_change_details - Enabled using both short and long IDs for playlist_change_details
- Added a cache handler to `SpotifyClientCredentials` - Added a cache handler to `SpotifyClientCredentials`
- Added the following endpoints - Added the following endpoints
- `Spotify.current_user_saved_episodes` - `Spotify.current_user_saved_episodes`
- `Spotify.current_user_saved_episodes_add` - `Spotify.current_user_saved_episodes_add`
- `Spotify.current_user_saved_episodes_delete` - `Spotify.current_user_saved_episodes_delete`
- `Spotify.current_user_saved_episodes_contains` - `Spotify.current_user_saved_episodes_contains`
- `Spotify.available_markets` - `Spotify.available_markets`
### Changed ### Changed
@ -240,7 +262,7 @@ Add your changes below.
### Added ### Added
- `SpotifyPKCE.parse_auth_response_url`, mirroring that method in - `SpotifyPKCE.parse_auth_response_url`, mirroring that method in
`SpotifyOAuth` `SpotifyOAuth`
### Changed ### Changed
@ -249,7 +271,7 @@ Add your changes below.
### Fixed ### Fixed
- Using `SpotifyPKCE.get_authorization_url` will now generate a code - Using `SpotifyPKCE.get_authorization_url` will now generate a code
challenge if needed challenge if needed
## [2.14.0] - 2020-08-29 ## [2.14.0] - 2020-08-29
@ -257,9 +279,9 @@ Add your changes below.
- (experimental) Support to search multiple/all markets at once. - (experimental) Support to search multiple/all markets at once.
- Support to test whether the current user is following certain - Support to test whether the current user is following certain
users or artists users or artists
- Proper replacements for all deprecated playlist endpoints - Proper replacements for all deprecated playlist endpoints
(See https://developer.spotify.com/community/news/2018/06/12/changes-to-playlist-uris/ and below) (See https://developer.spotify.com/community/news/2018/06/12/changes-to-playlist-uris/ and below)
- Allow for OAuth 2.0 authorization by instructing the user to open the URL in a browser instead of opening the browser. - Allow for OAuth 2.0 authorization by instructing the user to open the URL in a browser instead of opening the browser.
- Reason for 403 error in SpotifyException - Reason for 403 error in SpotifyException
- Support for the PKCE Auth Flow - Support for the PKCE Auth Flow
@ -277,11 +299,11 @@ Add your changes below.
- `user_playlist_replace_tracks` in favor of `playlist_replace_items` - `user_playlist_replace_tracks` in favor of `playlist_replace_items`
- `user_playlist_reorder_tracks` in favor of `playlist_reorder_items` - `user_playlist_reorder_tracks` in favor of `playlist_reorder_items`
- `user_playlist_remove_all_occurrences_of_tracks` in favor of - `user_playlist_remove_all_occurrences_of_tracks` in favor of
`playlist_remove_all_occurrences_of_items` `playlist_remove_all_occurrences_of_items`
- `user_playlist_remove_specific_occurrences_of_tracks` in favor of - `user_playlist_remove_specific_occurrences_of_tracks` in favor of
`playlist_remove_specific_occurrences_of_items` `playlist_remove_specific_occurrences_of_items`
- `user_playlist_follow_playlist` in favor of - `user_playlist_follow_playlist` in favor of
`current_user_follow_playlist` `current_user_follow_playlist`
- `user_playlist_is_following` in favor of `playlist_is_following` - `user_playlist_is_following` in favor of `playlist_is_following`
- `playlist_tracks` in favor of `playlist_items` - `playlist_tracks` in favor of `playlist_items`
@ -294,12 +316,12 @@ Add your changes below.
### Added ### Added
- Added `SpotifyImplicitGrant` as an auth manager option. It provides - Added `SpotifyImplicitGrant` as an auth manager option. It provides
user authentication without a client secret but sacrifices the ability user authentication without a client secret but sacrifices the ability
to refresh the token without user input. (However, read the class to refresh the token without user input. (However, read the class
docstring for security advisory.) docstring for security advisory.)
- Added built-in verification of the `state` query parameter - Added built-in verification of the `state` query parameter
- Added two new attributes: error and error_description to `SpotifyOauthError` exception class to show - Added two new attributes: error and error_description to `SpotifyOauthError` exception class to show
authorization/authentication web api errors details. authorization/authentication web api errors details.
- Added `SpotifyStateError` subclass of `SpotifyOauthError` - Added `SpotifyStateError` subclass of `SpotifyOauthError`
- Allow extending `SpotifyClientCredentials` and `SpotifyOAuth` - Allow extending `SpotifyClientCredentials` and `SpotifyOAuth`
- Added the market parameter to `album_tracks` - Added the market parameter to `album_tracks`
@ -323,10 +345,10 @@ Add your changes below.
### Changed ### Changed
- Updated the documentation to give more details on the authorization process and reflect - Updated the documentation to give more details on the authorization process and reflect
2020 Spotify Application jargon and practices. 2020 Spotify Application jargon and practices.
- The local webserver is only started for localhost redirect_uri which specify a port, - The local webserver is only started for localhost redirect_uri which specify a port,
i.e. it is started for `http://localhost:8080` or `http://127.0.0.1:8080`, not for `http://localhost`. i.e. it is started for `http://localhost:8080` or `http://127.0.0.1:8080`, not for `http://localhost`.
### Fixed ### Fixed
@ -349,10 +371,10 @@ Add your changes below.
- Client retry logic has changed as it now uses urllib3's `Retry` in conjunction with requests `Session` - Client retry logic has changed as it now uses urllib3's `Retry` in conjunction with requests `Session`
- The session is customizable as it allows for: - The session is customizable as it allows for:
- status_forcelist - status_forcelist
- retries - retries
- status_retries - status_retries
- backoff_factor - backoff_factor
- Spin up a local webserver to autofill authentication URL - Spin up a local webserver to autofill authentication URL
- Use session in SpotifyAuthBase - Use session in SpotifyAuthBase
- Logging used instead of print statements - Logging used instead of print statements
@ -367,9 +389,9 @@ Add your changes below.
### Added ### Added
- Support for `add_to_queue` - Support for `add_to_queue`
- **Parameters:** - **Parameters:**
- track uri, id, or url - track uri, id, or url
- device id. If None, then the active device is used. - device id. If None, then the active device is used.
- Add CHANGELOG and LICENSE to released package - Add CHANGELOG and LICENSE to released package
## [2.9.0] - 2020-02-15 ## [2.9.0] - 2020-02-15
@ -440,7 +462,7 @@ Add your changes below.
### Fixed ### Fixed
- Fixed inconsistent behaviour with some API methods when - Fixed inconsistent behaviour with some API methods when
a full HTTP URL is passed. a full HTTP URL is passed.
- Fixed invalid calls to logging warn method - Fixed invalid calls to logging warn method
### Removed ### Removed
@ -466,6 +488,7 @@ Add your changes below.
### Changed ### Changed
- Made instructions in the CONTRIBUTING.md file more clear such that it is easier to onboard and there are no conflicts with TUTORIAL.md - Made instructions in the CONTRIBUTING.md file more clear such that it is easier to onboard and there are no conflicts with TUTORIAL.md
## [2.5.0] - 2020-01-11 ## [2.5.0] - 2020-01-11
Added follow and player endpoints Added follow and player endpoints

@ -1 +0,0 @@
Subproject commit c610a79705ef4aa55e4d61572a012f77b6f7245d

View File

@ -41,7 +41,6 @@ class CacheHandler():
Save a token_info dictionary object to the cache and return None. Save a token_info dictionary object to the cache and return None.
""" """
raise NotImplementedError() raise NotImplementedError()
return None
class CacheFileHandler(CacheHandler): class CacheFileHandler(CacheHandler):
@ -77,24 +76,24 @@ class CacheFileHandler(CacheHandler):
token_info = None token_info = None
try: try:
f = open(self.cache_path) with open(self.cache_path, encoding='utf-8') as f:
token_info_string = f.read() token_info_string = f.read()
f.close()
token_info = json.loads(token_info_string) token_info = json.loads(token_info_string)
except OSError as error: except OSError as error:
if error.errno == errno.ENOENT: if error.errno == errno.ENOENT:
logger.debug(f"cache does not exist at: {self.cache_path}") logger.debug(f"cache does not exist at: {self.cache_path}")
else: else:
logger.warning(f"Couldn't read cache at: {self.cache_path}") logger.warning("Couldn't read cache at: %s", self.cache_path)
except json.JSONDecodeError:
logger.warning("Couldn't decode JSON from cache at: %s", self.cache_path)
return token_info return token_info
def save_token_to_cache(self, token_info): def save_token_to_cache(self, token_info):
try: try:
f = open(self.cache_path, "w") with open(self.cache_path, "w", encoding='utf-8') as f:
f.write(json.dumps(token_info, cls=self.encoder_cls)) f.write(json.dumps(token_info, cls=self.encoder_cls))
f.close()
except OSError: except OSError:
logger.warning(f"Couldn't write token to cache at: {self.cache_path}") logger.warning(f"Couldn't write token to cache at: {self.cache_path}")
@ -214,6 +213,7 @@ class RedisCacheHandler(CacheHandler):
class MemcacheCacheHandler(CacheHandler): class MemcacheCacheHandler(CacheHandler):
"""A Cache handler that stores the token info in Memcache using the pymemcache client """A Cache handler that stores the token info in Memcache using the pymemcache client
""" """
def __init__(self, memcache, key=None) -> None: def __init__(self, memcache, key=None) -> None:
""" """
Parameters: Parameters:

View File

@ -153,6 +153,7 @@ class Retry(urllib3.Retry):
""" """
Custom class for printing a warning when a rate/request limit is reached. Custom class for printing a warning when a rate/request limit is reached.
""" """
def increment( def increment(
self, self,
method: str | None = None, method: str | None = None,

View File

@ -52,18 +52,21 @@ class OAuthCacheTest(unittest.TestCase):
@patch('spotipy.cache_handler.open', create=True) @patch('spotipy.cache_handler.open', create=True)
def test_gets_from_cache_path(self, opener, def test_gets_from_cache_path(self, opener,
is_token_expired, refresh_access_token): is_token_expired, refresh_access_token):
"""Test that the token is retrieved from the cache path."""
scope = "playlist-modify-private" scope = "playlist-modify-private"
path = ".cache-username" path = ".cache-username"
tok = _make_fake_token(1, 1, scope) tok = _make_fake_token(1, 1, scope)
token_file = _token_file(json.dumps(tok, ensure_ascii=False))
opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False)) opener.return_value = token_file
opener.return_value.__enter__ = mock.Mock(return_value=token_file)
opener.return_value.__exit__ = mock.Mock(return_value=False)
is_token_expired.return_value = False is_token_expired.return_value = False
spot = _make_oauth(scope, path) spot = _make_oauth(scope, path)
cached_tok = spot.validate_token(spot.cache_handler.get_cached_token()) cached_tok = spot.validate_token(spot.cache_handler.get_cached_token())
cached_tok_legacy = spot.get_cached_token() cached_tok_legacy = spot.get_cached_token()
opener.assert_called_with(path) opener.assert_called_with(path, encoding='utf-8')
self.assertIsNotNone(cached_tok) self.assertIsNotNone(cached_tok)
self.assertIsNotNone(cached_tok_legacy) self.assertIsNotNone(cached_tok_legacy)
self.assertEqual(refresh_access_token.call_count, 0) self.assertEqual(refresh_access_token.call_count, 0)
@ -73,13 +76,15 @@ class OAuthCacheTest(unittest.TestCase):
@patch('spotipy.cache_handler.open', create=True) @patch('spotipy.cache_handler.open', create=True)
def test_expired_token_refreshes(self, opener, def test_expired_token_refreshes(self, opener,
is_token_expired, refresh_access_token): is_token_expired, refresh_access_token):
"""Test that an expired token is refreshed."""
scope = "playlist-modify-private" scope = "playlist-modify-private"
path = ".cache-username" path = ".cache-username"
expired_tok = _make_fake_token(0, None, scope) expired_tok = _make_fake_token(0, None, scope)
fresh_tok = _make_fake_token(1, 1, scope) fresh_tok = _make_fake_token(1, 1, scope)
token_file = _token_file(json.dumps(expired_tok, ensure_ascii=False)) token_file = _token_file(json.dumps(expired_tok, ensure_ascii=False))
opener.return_value = token_file opener.return_value.__enter__ = mock.Mock(return_value=token_file)
opener.return_value.__exit__ = mock.Mock(return_value=False)
refresh_access_token.return_value = fresh_tok refresh_access_token.return_value = fresh_tok
spot = _make_oauth(scope, path) spot = _make_oauth(scope, path)
@ -87,7 +92,7 @@ class OAuthCacheTest(unittest.TestCase):
is_token_expired.assert_called_with(expired_tok) is_token_expired.assert_called_with(expired_tok)
refresh_access_token.assert_called_with(expired_tok['refresh_token']) refresh_access_token.assert_called_with(expired_tok['refresh_token'])
opener.assert_any_call(path) opener.assert_any_call(path, encoding='utf-8')
@patch.multiple(SpotifyOAuth, @patch.multiple(SpotifyOAuth,
is_token_expired=DEFAULT, refresh_access_token=DEFAULT) is_token_expired=DEFAULT, refresh_access_token=DEFAULT)
@ -99,29 +104,35 @@ class OAuthCacheTest(unittest.TestCase):
path = ".cache-username" path = ".cache-username"
tok = _make_fake_token(1, 1, token_scope) tok = _make_fake_token(1, 1, token_scope)
opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False)) token_file = _token_file(json.dumps(tok, ensure_ascii=False))
opener.return_value = token_file
opener.return_value.__enter__ = mock.Mock(return_value=token_file)
opener.return_value.__exit__ = mock.Mock(return_value=False)
is_token_expired.return_value = False is_token_expired.return_value = False
spot = _make_oauth(requested_scope, path) spot = _make_oauth(requested_scope, path)
cached_tok = spot.validate_token(spot.cache_handler.get_cached_token()) cached_tok = spot.validate_token(spot.cache_handler.get_cached_token())
opener.assert_called_with(path) opener.assert_called_with(path, encoding='utf-8')
self.assertIsNone(cached_tok) self.assertIsNone(cached_tok)
self.assertEqual(refresh_access_token.call_count, 0) self.assertEqual(refresh_access_token.call_count, 0)
@patch('spotipy.cache_handler.open', create=True) @patch('spotipy.cache_handler.open', create=True)
def test_saves_to_cache_path(self, opener): def test_saves_to_cache_path(self, opener):
"""Test that the token is saved to the cache path."""
scope = "playlist-modify-private" scope = "playlist-modify-private"
path = ".cache-username" path = ".cache-username"
tok = _make_fake_token(1, 1, scope) tok = _make_fake_token(1, 1, scope)
fi = _fake_file() fi = _fake_file()
opener.return_value = fi opener.return_value = fi
opener.return_value.__enter__ = mock.Mock(return_value=fi)
opener.return_value.__exit__ = mock.Mock(return_value=False)
spot = SpotifyOAuth("CLID", "CLISEC", "REDIR", "STATE", scope, path) spot = SpotifyOAuth("CLID", "CLISEC", "REDIR", "STATE", scope, path)
spot.cache_handler.save_token_to_cache(tok) spot.cache_handler.save_token_to_cache(tok)
opener.assert_called_with(path, 'w') opener.assert_called_with(path, 'w', encoding='utf-8')
self.assertTrue(fi.write.called) self.assertTrue(fi.write.called)
@patch('spotipy.cache_handler.open', create=True) @patch('spotipy.cache_handler.open', create=True)
@ -132,11 +143,13 @@ class OAuthCacheTest(unittest.TestCase):
fi = _fake_file() fi = _fake_file()
opener.return_value = fi opener.return_value = fi
opener.return_value.__enter__ = mock.Mock(return_value=fi)
opener.return_value.__exit__ = mock.Mock(return_value=False)
spot = SpotifyOAuth("CLID", "CLISEC", "REDIR", "STATE", scope, path) spot = SpotifyOAuth("CLID", "CLISEC", "REDIR", "STATE", scope, path)
spot._save_token_info(tok) spot._save_token_info(tok)
opener.assert_called_with(path, 'w') opener.assert_called_with(path, 'w', encoding='utf-8')
self.assertTrue(fi.write.called) self.assertTrue(fi.write.called)
def test_cache_handler(self): def test_cache_handler(self):
@ -252,32 +265,38 @@ class ImplicitGrantCacheTest(unittest.TestCase):
path = ".cache-username" path = ".cache-username"
tok = _make_fake_token(1, 1, scope) tok = _make_fake_token(1, 1, scope)
opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False)) token_file = _token_file(json.dumps(tok, ensure_ascii=False))
opener.return_value = token_file
opener.return_value.__enter__ = mock.Mock(return_value=token_file)
opener.return_value.__exit__ = mock.Mock(return_value=False)
is_token_expired.return_value = False is_token_expired.return_value = False
spot = _make_implicitgrantauth(scope, path) spot = _make_implicitgrantauth(scope, path)
cached_tok = spot.cache_handler.get_cached_token() cached_tok = spot.cache_handler.get_cached_token()
cached_tok_legacy = spot.get_cached_token() cached_tok_legacy = spot.get_cached_token()
opener.assert_called_with(path) opener.assert_called_with(path, encoding='utf-8')
self.assertIsNotNone(cached_tok) self.assertIsNotNone(cached_tok)
self.assertIsNotNone(cached_tok_legacy) self.assertIsNotNone(cached_tok_legacy)
@patch.object(SpotifyImplicitGrant, "is_token_expired", DEFAULT) @patch.object(SpotifyImplicitGrant, "is_token_expired", DEFAULT)
@patch('spotipy.cache_handler.open', create=True) @patch('spotipy.cache_handler.open', create=True)
def test_expired_token_returns_none(self, opener, is_token_expired): def test_expired_token_returns_none(self, opener, is_token_expired):
"""Test that an expired token returns None."""
scope = "playlist-modify-private" scope = "playlist-modify-private"
path = ".cache-username" path = ".cache-username"
expired_tok = _make_fake_token(0, None, scope) expired_tok = _make_fake_token(0, None, scope)
token_file = _token_file(json.dumps(expired_tok, ensure_ascii=False)) token_file = _token_file(json.dumps(expired_tok, ensure_ascii=False))
opener.return_value = token_file opener.return_value = token_file
opener.return_value.__enter__ = mock.Mock(return_value=token_file)
opener.return_value.__exit__ = mock.Mock(return_value=False)
spot = _make_implicitgrantauth(scope, path) spot = _make_implicitgrantauth(scope, path)
cached_tok = spot.validate_token(spot.cache_handler.get_cached_token()) cached_tok = spot.validate_token(spot.cache_handler.get_cached_token())
is_token_expired.assert_called_with(expired_tok) is_token_expired.assert_called_with(expired_tok)
opener.assert_any_call(path) opener.assert_any_call(path, encoding='utf-8')
self.assertIsNone(cached_tok) self.assertIsNone(cached_tok)
@patch.object(SpotifyImplicitGrant, "is_token_expired", DEFAULT) @patch.object(SpotifyImplicitGrant, "is_token_expired", DEFAULT)
@ -288,13 +307,16 @@ class ImplicitGrantCacheTest(unittest.TestCase):
path = ".cache-username" path = ".cache-username"
tok = _make_fake_token(1, 1, token_scope) tok = _make_fake_token(1, 1, token_scope)
opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False)) token_file = _token_file(json.dumps(tok, ensure_ascii=False))
opener.return_value = token_file
opener.return_value.__enter__ = mock.Mock(return_value=token_file)
opener.return_value.__exit__ = mock.Mock(return_value=False)
is_token_expired.return_value = False is_token_expired.return_value = False
spot = _make_implicitgrantauth(requested_scope, path) spot = _make_implicitgrantauth(requested_scope, path)
cached_tok = spot.validate_token(spot.cache_handler.get_cached_token()) cached_tok = spot.validate_token(spot.cache_handler.get_cached_token())
opener.assert_called_with(path) opener.assert_called_with(path, encoding='utf-8')
self.assertIsNone(cached_tok) self.assertIsNone(cached_tok)
@patch('spotipy.cache_handler.open', create=True) @patch('spotipy.cache_handler.open', create=True)
@ -306,10 +328,12 @@ class ImplicitGrantCacheTest(unittest.TestCase):
fi = _fake_file() fi = _fake_file()
opener.return_value = fi opener.return_value = fi
opener.return_value.__enter__ = mock.Mock(return_value=fi)
opener.return_value.__exit__ = mock.Mock(return_value=False)
spot = SpotifyImplicitGrant("CLID", "REDIR", "STATE", scope, path) spot = SpotifyImplicitGrant("CLID", "REDIR", "STATE", scope, path)
spot.cache_handler.save_token_to_cache(tok) spot.cache_handler.save_token_to_cache(tok)
opener.assert_called_with(path, 'w') opener.assert_called_with(path, 'w', encoding='utf-8')
self.assertTrue(fi.write.called) self.assertTrue(fi.write.called)
@patch('spotipy.cache_handler.open', create=True) @patch('spotipy.cache_handler.open', create=True)
@ -320,11 +344,13 @@ class ImplicitGrantCacheTest(unittest.TestCase):
fi = _fake_file() fi = _fake_file()
opener.return_value = fi opener.return_value = fi
opener.return_value.__enter__ = mock.Mock(return_value=fi)
opener.return_value.__exit__ = mock.Mock(return_value=False)
spot = SpotifyImplicitGrant("CLID", "REDIR", "STATE", scope, path) spot = SpotifyImplicitGrant("CLID", "REDIR", "STATE", scope, path)
spot._save_token_info(tok) spot._save_token_info(tok)
opener.assert_called_with(path, 'w') opener.assert_called_with(path, 'w', encoding='utf-8')
self.assertTrue(fi.write.called) self.assertTrue(fi.write.called)
@ -389,14 +415,17 @@ class SpotifyPKCECacheTest(unittest.TestCase):
path = ".cache-username" path = ".cache-username"
tok = _make_fake_token(1, 1, scope) tok = _make_fake_token(1, 1, scope)
opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False)) token_file = _token_file(json.dumps(tok, ensure_ascii=False))
opener.return_value = token_file
opener.return_value.__enter__ = mock.Mock(return_value=token_file)
opener.return_value.__exit__ = mock.Mock(return_value=False)
is_token_expired.return_value = False is_token_expired.return_value = False
spot = _make_pkceauth(scope, path) spot = _make_pkceauth(scope, path)
cached_tok = spot.cache_handler.get_cached_token() cached_tok = spot.cache_handler.get_cached_token()
cached_tok_legacy = spot.get_cached_token() cached_tok_legacy = spot.get_cached_token()
opener.assert_called_with(path) opener.assert_called_with(path, encoding='utf-8')
self.assertIsNotNone(cached_tok) self.assertIsNotNone(cached_tok)
self.assertIsNotNone(cached_tok_legacy) self.assertIsNotNone(cached_tok_legacy)
self.assertEqual(refresh_access_token.call_count, 0) self.assertEqual(refresh_access_token.call_count, 0)
@ -412,7 +441,8 @@ class SpotifyPKCECacheTest(unittest.TestCase):
fresh_tok = _make_fake_token(1, 1, scope) fresh_tok = _make_fake_token(1, 1, scope)
token_file = _token_file(json.dumps(expired_tok, ensure_ascii=False)) token_file = _token_file(json.dumps(expired_tok, ensure_ascii=False))
opener.return_value = token_file opener.return_value.__enter__ = mock.Mock(return_value=token_file)
opener.return_value.__exit__ = mock.Mock(return_value=False)
refresh_access_token.return_value = fresh_tok refresh_access_token.return_value = fresh_tok
spot = _make_pkceauth(scope, path) spot = _make_pkceauth(scope, path)
@ -420,7 +450,7 @@ class SpotifyPKCECacheTest(unittest.TestCase):
is_token_expired.assert_called_with(expired_tok) is_token_expired.assert_called_with(expired_tok)
refresh_access_token.assert_called_with(expired_tok['refresh_token']) refresh_access_token.assert_called_with(expired_tok['refresh_token'])
opener.assert_any_call(path) opener.assert_any_call(path, encoding='utf-8')
@patch.multiple(SpotifyPKCE, @patch.multiple(SpotifyPKCE,
is_token_expired=DEFAULT, refresh_access_token=DEFAULT) is_token_expired=DEFAULT, refresh_access_token=DEFAULT)
@ -432,13 +462,16 @@ class SpotifyPKCECacheTest(unittest.TestCase):
path = ".cache-username" path = ".cache-username"
tok = _make_fake_token(1, 1, token_scope) tok = _make_fake_token(1, 1, token_scope)
opener.return_value = _token_file(json.dumps(tok, ensure_ascii=False)) token_file = _token_file(json.dumps(tok, ensure_ascii=False))
opener.return_value = token_file
opener.return_value.__enter__ = mock.Mock(return_value=token_file)
opener.return_value.__exit__ = mock.Mock(return_value=False)
is_token_expired.return_value = False is_token_expired.return_value = False
spot = _make_pkceauth(requested_scope, path) spot = _make_pkceauth(requested_scope, path)
cached_tok = spot.validate_token(spot.cache_handler.get_cached_token()) cached_tok = spot.validate_token(spot.cache_handler.get_cached_token())
opener.assert_called_with(path) opener.assert_called_with(path, encoding='utf-8')
self.assertIsNone(cached_tok) self.assertIsNone(cached_tok)
self.assertEqual(refresh_access_token.call_count, 0) self.assertEqual(refresh_access_token.call_count, 0)
@ -450,11 +483,12 @@ class SpotifyPKCECacheTest(unittest.TestCase):
fi = _fake_file() fi = _fake_file()
opener.return_value = fi opener.return_value = fi
opener.return_value.__enter__ = mock.Mock(return_value=fi)
opener.return_value.__exit__ = mock.Mock(return_value=False)
spot = SpotifyPKCE("CLID", "REDIR", "STATE", scope, path) spot = SpotifyPKCE("CLID", "REDIR", "STATE", scope, path)
spot.cache_handler.save_token_to_cache(tok) spot.cache_handler.save_token_to_cache(tok)
opener.assert_called_with(path, 'w') opener.assert_called_with(path, 'w', encoding='utf-8')
self.assertTrue(fi.write.called) self.assertTrue(fi.write.called)
@patch('spotipy.cache_handler.open', create=True) @patch('spotipy.cache_handler.open', create=True)
@ -465,11 +499,13 @@ class SpotifyPKCECacheTest(unittest.TestCase):
fi = _fake_file() fi = _fake_file()
opener.return_value = fi opener.return_value = fi
opener.return_value.__enter__ = mock.Mock(return_value=fi)
opener.return_value.__exit__ = mock.Mock(return_value=False)
spot = SpotifyPKCE("CLID", "REDIR", "STATE", scope, path) spot = SpotifyPKCE("CLID", "REDIR", "STATE", scope, path)
spot._save_token_info(tok) spot._save_token_info(tok)
opener.assert_called_with(path, 'w') opener.assert_called_with(path, 'w', encoding='utf-8')
self.assertTrue(fi.write.called) self.assertTrue(fi.write.called)