Drop support for EOL Python 3.7 (#1065)

* Add python_requires to help pip

* Update supported versions in tox.ini

* Upgrade Python syntax with pyupgrade --py37-plus

* Bump GitHub Actions

* Add Python 3.11 and 3.12 to CI

* Remove six dependency

* Remove redundant dependencies

* Remove redudant Python 3.5 code

* Drop support for EOL Python 3.7

* Upgrade Python syntax with pyupgrade --py38-plus

* Update CHANGELOG

* More f-strings

---------

Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>
This commit is contained in:
Hugo van Kemenade 2024-05-21 12:32:01 -04:00 committed by GitHub
parent 958ff6ad2b
commit 85c9d74dc1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 85 additions and 125 deletions

View File

@ -11,9 +11,9 @@ jobs:
SPOTIPY_CLIENT_SECRET: ${{ secrets.SPOTIPY_CLIENT_SECRET }} SPOTIPY_CLIENT_SECRET: ${{ secrets.SPOTIPY_CLIENT_SECRET }}
PYTHON_VERSION: "3.10" PYTHON_VERSION: "3.10"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v1 uses: actions/setup-python@v5
with: with:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies - name: Install dependencies

View File

@ -13,9 +13,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: "3.x" python-version: "3.x"
- name: Install pypa/build - name: Install pypa/build
@ -33,7 +33,7 @@ jobs:
--outdir dist/ --outdir dist/
. .
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: "2.x" python-version: "2.x"
- name: Install pypa/build - name: Install pypa/build

View File

@ -8,8 +8,8 @@ jobs:
changelog: changelog:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v4
- uses: dangoslen/changelog-enforcer@v1.1.1 - uses: dangoslen/changelog-enforcer@v3.5.1
with: with:
changeLogPath: 'CHANGELOG.md' changeLogPath: 'CHANGELOG.md'
skipLabel: 'skip-changelog' skipLabel: 'skip-changelog'

View File

@ -8,11 +8,11 @@ jobs:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
strategy: strategy:
matrix: matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies

View File

@ -30,6 +30,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Fixed unused description parameter in playlist creation example - Fixed unused description parameter in playlist creation example
### Changed
- Drop support for EOL Python 3.7.
## [2.23.0] - 2023-04-07 ## [2.23.0] - 2023-04-07
### Added ### Added

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# spotipy documentation build configuration file, created by # spotipy documentation build configuration file, created by
# sphinx-quickstart on Thu Aug 21 11:04:39 2014. # sphinx-quickstart on Thu Aug 21 11:04:39 2014.

View File

@ -333,7 +333,7 @@ Export the needed Environment variables:::
export SPOTIPY_REDIRECT_URI=http://localhost:8080 # Make url is set in app you created to get your ID and SECRET export SPOTIPY_REDIRECT_URI=http://localhost:8080 # Make url is set in app you created to get your ID and SECRET
Create virtual environment, install dependencies, run tests::: Create virtual environment, install dependencies, run tests:::
$ virtualenv --python=python3.7 env $ virtualenv --python=python3.12 env
(env) $ pip install --user -e . (env) $ pip install --user -e .
(env) $ python -m unittest discover -v tests (env) $ python -m unittest discover -v tests

View File

@ -1,6 +1,5 @@
# shows audio analysis for the given track # shows audio analysis for the given track
from __future__ import print_function # (at top of module)
from spotipy.oauth2 import SpotifyClientCredentials from spotipy.oauth2 import SpotifyClientCredentials
import json import json
import spotipy import spotipy
@ -20,4 +19,4 @@ start = time.time()
analysis = sp.audio_analysis(tid) analysis = sp.audio_analysis(tid)
delta = time.time() - start delta = time.time() - start
print(json.dumps(analysis, indent=4)) print(json.dumps(analysis, indent=4))
print("analysis retrieved in %.2f seconds" % (delta,)) print(f"analysis retrieved in {delta:.2f} seconds")

View File

@ -1,7 +1,5 @@
# shows acoustic features for tracks for the given artist # shows acoustic features for tracks for the given artist
from __future__ import print_function # (at top of module)
from spotipy.oauth2 import SpotifyClientCredentials from spotipy.oauth2 import SpotifyClientCredentials
import json import json
import spotipy import spotipy
@ -33,4 +31,4 @@ for feature in features:
analysis = sp._get(feature['analysis_url']) analysis = sp._get(feature['analysis_url'])
print(json.dumps(analysis, indent=4)) print(json.dumps(analysis, indent=4))
print() print()
print("features retrieved in %.2f seconds" % (delta,)) print(f"features retrieved in {delta:.2f} seconds")

View File

@ -1,8 +1,5 @@
# shows acoustic features for tracks for the given artist # shows acoustic features for tracks for the given artist
from __future__ import print_function # (at top of module)
from spotipy.oauth2 import SpotifyClientCredentials from spotipy.oauth2 import SpotifyClientCredentials
import json import json
import spotipy import spotipy
@ -22,4 +19,4 @@ if len(sys.argv) > 1:
features = sp.audio_features(tids) features = sp.audio_features(tids)
delta = time.time() - start delta = time.time() - start
print(json.dumps(features, indent=4)) print(json.dumps(features, indent=4))
print("features retrieved in %.2f seconds" % (delta,)) print(f"features retrieved in {delta:.2f} seconds")

View File

@ -11,7 +11,7 @@ scope = 'user-library-read'
if len(sys.argv) > 1: if len(sys.argv) > 1:
tid = sys.argv[1] tid = sys.argv[1]
else: else:
print("Usage: %s track-id ..." % (sys.argv[0],)) print(f"Usage: {sys.argv[0]} track-id ...")
sys.exit() sys.exit()
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope)) sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))

View File

@ -11,7 +11,7 @@ scope = 'user-library-modify'
if len(sys.argv) > 1: if len(sys.argv) > 1:
tid = sys.argv[1] tid = sys.argv[1]
else: else:
print("Usage: %s track-id ..." % (sys.argv[0],)) print(f"Usage: {sys.argv[0]} track-id ...")
sys.exit() sys.exit()
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope)) sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))

View File

@ -15,8 +15,7 @@ if len(sys.argv) > 2:
track_ids.append({"uri": tid, "positions": [int(pos)]}) track_ids.append({"uri": tid, "positions": [int(pos)]})
else: else:
print( print(
"Usage: %s playlist_id track_id,pos track_id,pos ..." % f"Usage: {sys.argv[0]} playlist_id track_id,pos track_id,pos ...")
(sys.argv[0],))
sys.exit() sys.exit()
scope = 'playlist-modify-public' scope = 'playlist-modify-public'

View File

@ -10,7 +10,7 @@ if len(sys.argv) > 2:
playlist_id = sys.argv[2] playlist_id = sys.argv[2]
track_ids = sys.argv[3:] track_ids = sys.argv[3:]
else: else:
print("Usage: %s playlist_id track_id ..." % (sys.argv[0])) print(f"Usage: {sys.argv[0]} playlist_id track_id ...")
sys.exit() sys.exit()
scope = 'playlist-modify-public' scope = 'playlist-modify-public'

View File

@ -10,7 +10,7 @@ if len(sys.argv) > 3:
playlist_id = sys.argv[1] playlist_id = sys.argv[1]
track_ids = sys.argv[2:] track_ids = sys.argv[2:]
else: else:
print("Usage: %s playlist_id track_id ..." % (sys.argv[0],)) print(f"Usage: {sys.argv[0]} playlist_id track_id ...")
sys.exit() sys.exit()
scope = 'playlist-modify-public' scope = 'playlist-modify-public'

View File

@ -1,4 +1,3 @@
# shows album info for a URN or URL # shows album info for a URN or URL
from spotipy.oauth2 import SpotifyClientCredentials from spotipy.oauth2 import SpotifyClientCredentials

View File

@ -1,4 +1,3 @@
# shows related artists for the given seed artist # shows related artists for the given seed artist
from spotipy.oauth2 import SpotifyClientCredentials from spotipy.oauth2 import SpotifyClientCredentials

View File

@ -13,4 +13,4 @@ while results['next']:
albums.extend(results['items']) albums.extend(results['items'])
for album in albums: for album in albums:
print((album['name'])) print(album['name'])

View File

@ -1,4 +1,3 @@
from spotipy.oauth2 import SpotifyClientCredentials from spotipy.oauth2 import SpotifyClientCredentials
import spotipy import spotipy

View File

@ -13,7 +13,7 @@ client_credentials_manager = SpotifyClientCredentials()
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
skiplist = set(['dm', 'remix']) skiplist = {'dm', 'remix'}
max_offset = 500 max_offset = 500
seen = set() seen = set()

View File

@ -1,6 +1,6 @@
from setuptools import setup from setuptools import setup
with open("README.md", "r") as f: with open("README.md") as f:
long_description = f.read() long_description = f.read()
test_reqs = [ test_reqs = [
@ -28,11 +28,10 @@ setup(
project_urls={ project_urls={
'Source': 'https://github.com/plamere/spotipy', 'Source': 'https://github.com/plamere/spotipy',
}, },
python_requires='>3.8',
install_requires=[ install_requires=[
"redis>=3.5.3", "redis>=3.5.3",
"redis<4.0.0;python_version<'3.4'",
"requests>=2.25.0", "requests>=2.25.0",
"six>=1.15.0",
"urllib3>=1.26.0" "urllib3>=1.26.0"
], ],
tests_require=test_reqs, tests_require=test_reqs,

View File

@ -80,7 +80,7 @@ class CacheFileHandler(CacheHandler):
f.close() f.close()
token_info = json.loads(token_info_string) token_info = json.loads(token_info_string)
except IOError as error: except OSError as error:
if error.errno == errno.ENOENT: if error.errno == errno.ENOENT:
logger.debug("cache does not exist at: %s", self.cache_path) logger.debug("cache does not exist at: %s", self.cache_path)
else: else:
@ -93,7 +93,7 @@ class CacheFileHandler(CacheHandler):
f = open(self.cache_path, "w") f = open(self.cache_path, "w")
f.write(json.dumps(token_info, cls=self.encoder_cls)) f.write(json.dumps(token_info, cls=self.encoder_cls))
f.close() f.close()
except IOError: except OSError:
logger.warning('Couldn\'t write token to cache at: %s', logger.warning('Couldn\'t write token to cache at: %s',
self.cache_path) self.cache_path)

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" A simple and thin Python library for the Spotify Web API """ """ A simple and thin Python library for the Spotify Web API """
__all__ = ["Spotify", "SpotifyException"] __all__ = ["Spotify", "SpotifyException"]
@ -10,7 +8,6 @@ import re
import warnings import warnings
import requests import requests
import six
import urllib3 import urllib3
from spotipy.exceptions import SpotifyException from spotipy.exceptions import SpotifyException
@ -20,7 +17,7 @@ from collections import defaultdict
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Spotify(object): class Spotify:
""" """
Example usage:: Example usage::
@ -234,14 +231,14 @@ class Spotify(object):
def _auth_headers(self): def _auth_headers(self):
if self._auth: if self._auth:
return {"Authorization": "Bearer {0}".format(self._auth)} return {"Authorization": f"Bearer {self._auth}"}
if not self.auth_manager: if not self.auth_manager:
return {} return {}
try: try:
token = self.auth_manager.get_access_token(as_dict=False) token = self.auth_manager.get_access_token(as_dict=False)
except TypeError: except TypeError:
token = self.auth_manager.get_access_token() token = self.auth_manager.get_access_token()
return {"Authorization": "Bearer {0}".format(token)} return {"Authorization": f"Bearer {token}"}
def _internal_call(self, method, url, payload, params): def _internal_call(self, method, url, payload, params):
args = dict(params=params) args = dict(params=params)
@ -296,7 +293,7 @@ class Spotify(object):
raise SpotifyException( raise SpotifyException(
response.status_code, response.status_code,
-1, -1,
"%s:\n %s" % (response.url, msg), f"{response.url}:\n {msg}",
reason=reason, reason=reason,
headers=response.headers, headers=response.headers,
) )
@ -310,7 +307,7 @@ class Spotify(object):
raise SpotifyException( raise SpotifyException(
429, 429,
-1, -1,
"%s:\n %s" % (request.path_url, "Max Retries"), f"{request.path_url}:\n Max Retries",
reason=reason reason=reason
) )
except ValueError: except ValueError:
@ -663,7 +660,7 @@ class Spotify(object):
""" """
plid = self._get_id("playlist", playlist_id) plid = self._get_id("playlist", playlist_id)
return self._get( return self._get(
"playlists/%s" % (plid), f"playlists/{plid}",
fields=fields, fields=fields,
market=market, market=market,
additional_types=",".join(additional_types), additional_types=",".join(additional_types),
@ -719,7 +716,7 @@ class Spotify(object):
""" """
plid = self._get_id("playlist", playlist_id) plid = self._get_id("playlist", playlist_id)
return self._get( return self._get(
"playlists/%s/tracks" % (plid), f"playlists/{plid}/tracks",
limit=limit, limit=limit,
offset=offset, offset=offset,
fields=fields, fields=fields,
@ -734,7 +731,7 @@ class Spotify(object):
- playlist_id - the playlist ID, URI or URL - playlist_id - the playlist ID, URI or URL
""" """
plid = self._get_id("playlist", playlist_id) plid = self._get_id("playlist", playlist_id)
return self._get("playlists/%s/images" % (plid)) return self._get(f"playlists/{plid}/images")
def playlist_upload_cover_image(self, playlist_id, image_b64): def playlist_upload_cover_image(self, playlist_id, image_b64):
""" Replace the image used to represent a specific playlist """ Replace the image used to represent a specific playlist
@ -746,7 +743,7 @@ class Spotify(object):
""" """
plid = self._get_id("playlist", playlist_id) plid = self._get_id("playlist", playlist_id)
return self._put( return self._put(
"playlists/{}/images".format(plid), f"playlists/{plid}/images",
payload=image_b64, payload=image_b64,
content_type="image/jpeg", content_type="image/jpeg",
) )
@ -765,7 +762,7 @@ class Spotify(object):
- fields - which fields to return - fields - which fields to return
""" """
if playlist_id is None: if playlist_id is None:
return self._get("users/%s/starred" % user) return self._get(f"users/{user}/starred")
return self.playlist(playlist_id, fields=fields, market=market) return self.playlist(playlist_id, fields=fields, market=market)
def user_playlist_tracks( def user_playlist_tracks(
@ -809,7 +806,7 @@ class Spotify(object):
- offset - the index of the first item to return - offset - the index of the first item to return
""" """
return self._get( return self._get(
"users/%s/playlists" % user, limit=limit, offset=offset f"users/{user}/playlists", limit=limit, offset=offset
) )
def user_playlist_create(self, user, name, public=True, collaborative=False, description=""): def user_playlist_create(self, user, name, public=True, collaborative=False, description=""):
@ -829,7 +826,7 @@ class Spotify(object):
"description": description "description": description
} }
return self._post("users/%s/playlists" % (user,), payload=data) return self._post(f"users/{user}/playlists", payload=data)
def user_playlist_change_details( def user_playlist_change_details(
self, self,
@ -1004,7 +1001,7 @@ class Spotify(object):
if snapshot_id: if snapshot_id:
payload["snapshot_id"] = snapshot_id payload["snapshot_id"] = snapshot_id
return self._delete( return self._delete(
"users/%s/playlists/%s/tracks" % (user, plid), payload=payload f"users/{user}/playlists/{plid}/tracks", payload=payload
) )
def user_playlist_follow_playlist(self, playlist_owner_id, playlist_id): def user_playlist_follow_playlist(self, playlist_owner_id, playlist_id):
@ -1061,16 +1058,16 @@ class Spotify(object):
""" """
data = {} data = {}
if isinstance(name, six.string_types): if isinstance(name, str):
data["name"] = name data["name"] = name
if isinstance(public, bool): if isinstance(public, bool):
data["public"] = public data["public"] = public
if isinstance(collaborative, bool): if isinstance(collaborative, bool):
data["collaborative"] = collaborative data["collaborative"] = collaborative
if isinstance(description, six.string_types): if isinstance(description, str):
data["description"] = description data["description"] = description
return self._put( return self._put(
"playlists/%s" % (self._get_id("playlist", playlist_id)), payload=data f"playlists/{self._get_id('playlist', playlist_id)}", payload=data
) )
def current_user_unfollow_playlist(self, playlist_id): def current_user_unfollow_playlist(self, playlist_id):
@ -1081,7 +1078,7 @@ class Spotify(object):
- name - the name of the playlist - name - the name of the playlist
""" """
return self._delete( return self._delete(
"playlists/%s/followers" % (playlist_id) f"playlists/{playlist_id}/followers"
) )
def playlist_add_items( def playlist_add_items(
@ -1097,7 +1094,7 @@ class Spotify(object):
plid = self._get_id("playlist", playlist_id) plid = self._get_id("playlist", playlist_id)
ftracks = [self._get_uri("track", tid) for tid in items] ftracks = [self._get_uri("track", tid) for tid in items]
return self._post( return self._post(
"playlists/%s/tracks" % (plid), f"playlists/{plid}/tracks",
payload=ftracks, payload=ftracks,
position=position, position=position,
) )
@ -1113,7 +1110,7 @@ class Spotify(object):
ftracks = [self._get_uri("track", tid) for tid in items] ftracks = [self._get_uri("track", tid) for tid in items]
payload = {"uris": ftracks} payload = {"uris": ftracks}
return self._put( return self._put(
"playlists/%s/tracks" % (plid), payload=payload f"playlists/{plid}/tracks", payload=payload
) )
def playlist_reorder_items( def playlist_reorder_items(
@ -1144,7 +1141,7 @@ class Spotify(object):
if snapshot_id: if snapshot_id:
payload["snapshot_id"] = snapshot_id payload["snapshot_id"] = snapshot_id
return self._put( return self._put(
"playlists/%s/tracks" % (plid), payload=payload f"playlists/{plid}/tracks", payload=payload
) )
def playlist_remove_all_occurrences_of_items( def playlist_remove_all_occurrences_of_items(
@ -1165,7 +1162,7 @@ class Spotify(object):
if snapshot_id: if snapshot_id:
payload["snapshot_id"] = snapshot_id payload["snapshot_id"] = snapshot_id
return self._delete( return self._delete(
"playlists/%s/tracks" % (plid), payload=payload f"playlists/{plid}/tracks", payload=payload
) )
def playlist_remove_specific_occurrences_of_items( def playlist_remove_specific_occurrences_of_items(
@ -1196,7 +1193,7 @@ class Spotify(object):
if snapshot_id: if snapshot_id:
payload["snapshot_id"] = snapshot_id payload["snapshot_id"] = snapshot_id
return self._delete( return self._delete(
"playlists/%s/tracks" % (plid), payload=payload f"playlists/{plid}/tracks", payload=payload
) )
def current_user_follow_playlist(self, playlist_id): def current_user_follow_playlist(self, playlist_id):
@ -1208,7 +1205,7 @@ class Spotify(object):
""" """
return self._put( return self._put(
"playlists/{}/followers".format(playlist_id) f"playlists/{playlist_id}/followers"
) )
def playlist_is_following( def playlist_is_following(
@ -1874,7 +1871,7 @@ class Spotify(object):
return return
return self._put( return self._put(
self._append_device_id( self._append_device_id(
"me/player/seek?position_ms=%s" % position_ms, device_id f"me/player/seek?position_ms={position_ms}", device_id
) )
) )
@ -1890,7 +1887,7 @@ class Spotify(object):
return return
self._put( self._put(
self._append_device_id( self._append_device_id(
"me/player/repeat?state=%s" % state, device_id f"me/player/repeat?state={state}", device_id
) )
) )
@ -1909,7 +1906,7 @@ class Spotify(object):
return return
self._put( self._put(
self._append_device_id( self._append_device_id(
"me/player/volume?volume_percent=%s" % volume_percent, f"me/player/volume?volume_percent={volume_percent}",
device_id, device_id,
) )
) )
@ -1927,7 +1924,7 @@ class Spotify(object):
state = str(state).lower() state = str(state).lower()
self._put( self._put(
self._append_device_id( self._append_device_id(
"me/player/shuffle?state=%s" % state, device_id f"me/player/shuffle?state={state}", device_id
) )
) )
@ -1952,10 +1949,10 @@ class Spotify(object):
uri = self._get_uri("track", uri) uri = self._get_uri("track", uri)
endpoint = "me/player/queue?uri=%s" % uri endpoint = f"me/player/queue?uri={uri}"
if device_id is not None: if device_id is not None:
endpoint += "&device_id=%s" % device_id endpoint += f"&device_id={device_id}"
return self._post(endpoint) return self._post(endpoint)
@ -1974,9 +1971,9 @@ class Spotify(object):
""" """
if device_id: if device_id:
if "?" in path: if "?" in path:
path += "&device_id=%s" % device_id path += f"&device_id={device_id}"
else: else:
path += "?device_id=%s" % device_id path += f"?device_id={device_id}"
return path return path
def _get_id(self, type, id): def _get_id(self, type, id):

View File

@ -12,5 +12,5 @@ class SpotifyException(Exception):
self.headers = headers self.headers = headers
def __str__(self): def __str__(self):
return 'http status: {0}, code:{1} - {2}, reason: {3}'.format( return 'http status: {}, code:{} - {}, reason: {}'.format(
self.http_status, self.code, self.msg, self.reason) self.http_status, self.code, self.msg, self.reason)

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
__all__ = [ __all__ = [
"SpotifyClientCredentials", "SpotifyClientCredentials",
"SpotifyOAuth", "SpotifyOAuth",
@ -17,11 +15,9 @@ import warnings
import webbrowser import webbrowser
import requests import requests
# Workaround to support both python 2 & 3 import urllib.parse as urllibparse
import six from http.server import BaseHTTPRequestHandler, HTTPServer
import six.moves.urllib.parse as urllibparse from urllib.parse import parse_qsl, urlparse
from six.moves.BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from six.moves.urllib_parse import parse_qsl, urlparse
from spotipy.cache_handler import CacheFileHandler, CacheHandler from spotipy.cache_handler import CacheFileHandler, CacheHandler
from spotipy.util import CLIENT_CREDS_ENV_VARS, get_host_port, normalize_scope from spotipy.util import CLIENT_CREDS_ENV_VARS, get_host_port, normalize_scope
@ -36,7 +32,7 @@ class SpotifyOauthError(Exception):
self.error = error self.error = error
self.error_description = error_description self.error_description = error_description
self.__dict__.update(kwargs) self.__dict__.update(kwargs)
super(SpotifyOauthError, self).__init__(message, *args, **kwargs) super().__init__(message, *args, **kwargs)
class SpotifyStateError(SpotifyOauthError): class SpotifyStateError(SpotifyOauthError):
@ -54,24 +50,21 @@ class SpotifyStateError(SpotifyOauthError):
def _make_authorization_headers(client_id, client_secret): def _make_authorization_headers(client_id, client_secret):
auth_header = base64.b64encode( auth_header = base64.b64encode(
six.text_type(client_id + ":" + client_secret).encode("ascii") str(client_id + ":" + client_secret).encode("ascii")
) )
return {"Authorization": "Basic %s" % auth_header.decode("ascii")} return {"Authorization": f"Basic {auth_header.decode('ascii')}"}
def _ensure_value(value, env_key): def _ensure_value(value, env_key):
env_val = CLIENT_CREDS_ENV_VARS[env_key] env_val = CLIENT_CREDS_ENV_VARS[env_key]
_val = value or os.getenv(env_val) _val = value or os.getenv(env_val)
if _val is None: if _val is None:
msg = "No %s. Pass it or set a %s environment variable." % ( msg = f"No {env_key}. Pass it or set a {env_val} environment variable."
env_key,
env_val,
)
raise SpotifyOauthError(msg) raise SpotifyOauthError(msg)
return _val return _val
class SpotifyAuthBase(object): class SpotifyAuthBase:
def __init__(self, requests_session): def __init__(self, requests_session):
if isinstance(requests_session, requests.Session): if isinstance(requests_session, requests.Session):
self._session = requests_session self._session = requests_session
@ -144,9 +137,7 @@ class SpotifyAuthBase(object):
error_description = None error_description = None
raise SpotifyOauthError( raise SpotifyOauthError(
'error: {0}, error_description: {1}'.format( f'error: {error}, error_description: {error_description}',
error, error_description
),
error=error, error=error,
error_description=error_description error_description=error_description
) )
@ -196,7 +187,7 @@ class SpotifyClientCredentials(SpotifyAuthBase):
""" """
super(SpotifyClientCredentials, self).__init__(requests_session) super().__init__(requests_session)
self.client_id = client_id self.client_id = client_id
self.client_secret = client_secret self.client_secret = client_secret
@ -327,7 +318,7 @@ class SpotifyOAuth(SpotifyAuthBase):
(takes precedence over `cache_path` and `username`) (takes precedence over `cache_path` and `username`)
""" """
super(SpotifyOAuth, self).__init__(requests_session) super().__init__(requests_session)
self.client_id = client_id self.client_id = client_id
self.client_secret = client_secret self.client_secret = client_secret
@ -402,7 +393,7 @@ class SpotifyOAuth(SpotifyAuthBase):
urlparams = urllibparse.urlencode(payload) urlparams = urllibparse.urlencode(payload)
return "%s?%s" % (self.OAUTH_AUTHORIZE_URL, urlparams) return f"{self.OAUTH_AUTHORIZE_URL}?{urlparams}"
def parse_response_code(self, url): def parse_response_code(self, url):
""" Parse the response code in the given response url """ Parse the response code in the given response url
@ -421,8 +412,7 @@ class SpotifyOAuth(SpotifyAuthBase):
query_s = urlparse(url).query query_s = urlparse(url).query
form = dict(parse_qsl(query_s)) form = dict(parse_qsl(query_s))
if "error" in form: if "error" in form:
raise SpotifyOauthError("Received error from auth server: " raise SpotifyOauthError(f"Received error from auth server: {form['error']}",
"{}".format(form["error"]),
error=form["error"]) error=form["error"])
return tuple(form.get(param) for param in ["state", "code"]) return tuple(form.get(param) for param in ["state", "code"])
@ -677,7 +667,7 @@ class SpotifyPKCE(SpotifyAuthBase):
(takes precedence over `cache_path` and `username`) (takes precedence over `cache_path` and `username`)
""" """
super(SpotifyPKCE, self).__init__(requests_session) super().__init__(requests_session)
self.client_id = client_id self.client_id = client_id
self.redirect_uri = redirect_uri self.redirect_uri = redirect_uri
self.state = state self.state = state
@ -727,15 +717,8 @@ class SpotifyPKCE(SpotifyAuthBase):
length = random.randint(33, 96) length = random.randint(33, 96)
# The seeded length generates between a 44 and 128 base64 characters encoded string # The seeded length generates between a 44 and 128 base64 characters encoded string
try: import secrets
import secrets return secrets.token_urlsafe(length)
verifier = secrets.token_urlsafe(length)
except ImportError: # For python 3.5 support
import base64
import os
rand_bytes = os.urandom(length)
verifier = base64.urlsafe_b64encode(rand_bytes).decode('utf-8').replace('=', '')
return verifier
def _get_code_challenge(self): def _get_code_challenge(self):
""" Spotify PCKE code challenge - See step 1 of the reference guide below """ Spotify PCKE code challenge - See step 1 of the reference guide below
@ -766,7 +749,7 @@ class SpotifyPKCE(SpotifyAuthBase):
if state is not None: if state is not None:
payload["state"] = state payload["state"] = state
urlparams = urllibparse.urlencode(payload) urlparams = urllibparse.urlencode(payload)
return "%s?%s" % (self.OAUTH_AUTHORIZE_URL, urlparams) return f"{self.OAUTH_AUTHORIZE_URL}?{urlparams}"
def _open_auth_url(self, state=None): def _open_auth_url(self, state=None):
auth_url = self.get_authorize_url(state) auth_url = self.get_authorize_url(state)
@ -817,7 +800,7 @@ class SpotifyPKCE(SpotifyAuthBase):
if server.auth_code is not None: if server.auth_code is not None:
return server.auth_code return server.auth_code
elif server.error is not None: elif server.error is not None:
raise SpotifyOauthError("Received error from OAuth server: {}".format(server.error)) raise SpotifyOauthError(f"Received error from OAuth server: {server.error}")
else: else:
raise SpotifyOauthError("Server listening on localhost has not been accessed") raise SpotifyOauthError("Server listening on localhost has not been accessed")
@ -1160,7 +1143,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
urlparams = urllibparse.urlencode(payload) urlparams = urllibparse.urlencode(payload)
return "%s?%s" % (self.OAUTH_AUTHORIZE_URL, urlparams) return f"{self.OAUTH_AUTHORIZE_URL}?{urlparams}"
def parse_response_token(self, url, state=None): def parse_response_token(self, url, state=None):
""" Parse the response code in the given response url """ """ Parse the response code in the given response url """
@ -1180,8 +1163,7 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
form = dict(i.split('=') for i form = dict(i.split('=') for i
in (fragment_s or query_s or url).split('&')) in (fragment_s or query_s or url).split('&'))
if "error" in form: if "error" in form:
raise SpotifyOauthError("Received error from auth server: " raise SpotifyOauthError(f"Received error from auth server: {form['error']}",
"{}".format(form["error"]),
state=form["state"]) state=form["state"])
if "expires_in" in form: if "expires_in" in form:
form["expires_in"] = int(form["expires_in"]) form["expires_in"] = int(form["expires_in"])
@ -1273,7 +1255,7 @@ class RequestHandler(BaseHTTPRequestHandler):
if self.server.auth_code: if self.server.auth_code:
status = "successful" status = "successful"
elif self.server.error: elif self.server.error:
status = "failed ({})".format(self.server.error) status = f"failed ({self.server.error})"
else: else:
self._write("<html><body><h1>Invalid request</h1></body></html>") self._write("<html><body><h1>Invalid request</h1></body></html>")
return return

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
""" Shows a user's playlists (need to be authenticated via oauth) """ """ Shows a user's playlists (need to be authenticated via oauth) """
__all__ = ["CLIENT_CREDS_ENV_VARS", "prompt_for_user_token"] __all__ = ["CLIENT_CREDS_ENV_VARS", "prompt_for_user_token"]

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from spotipy import ( from spotipy import (
Spotify, Spotify,
SpotifyClientCredentials, SpotifyClientCredentials,

View File

@ -1,20 +1,15 @@
# -*- coding: utf-8 -*-
import io import io
import json import json
import unittest import unittest
import six.moves.urllib.parse as urllibparse import unittest.mock as mock
import urllib.parse as urllibparse
from spotipy import SpotifyOAuth, SpotifyImplicitGrant, SpotifyPKCE from spotipy import SpotifyOAuth, SpotifyImplicitGrant, SpotifyPKCE
from spotipy.cache_handler import MemoryCacheHandler from spotipy.cache_handler import MemoryCacheHandler
from spotipy.oauth2 import SpotifyClientCredentials, SpotifyOauthError from spotipy.oauth2 import SpotifyClientCredentials, SpotifyOauthError
from spotipy.oauth2 import SpotifyStateError from spotipy.oauth2 import SpotifyStateError
try:
import unittest.mock as mock
except ImportError:
import mock
patch = mock.patch patch = mock.patch
DEFAULT = mock.DEFAULT DEFAULT = mock.DEFAULT

View File

@ -1,10 +1,8 @@
[tox] [tox]
envlist = py27,py34 envlist = py3{8,9,10,11,12}
[testenv] [testenv]
deps= deps=
requests requests
six
py27: mock
commands=python -m unittest discover -v tests commands=python -m unittest discover -v tests
[flake8] [flake8]
max-line-length = 99 max-line-length = 99