mirror of
https://github.com/spotipy-dev/spotipy.git
synced 2026-06-19 09:13:53 +00:00
Merge branch 'v3' into master
This commit is contained in:
commit
aa9652f1fd
2
.github/workflows/pythonapp.yml
vendored
2
.github/workflows/pythonapp.yml
vendored
@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [2.7, 3.5, 3.6, 3.7, 3.8]
|
||||
python-version: [3.5, 3.6, 3.7, 3.8, 3.9]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
55
CHANGELOG.md
55
CHANGELOG.md
@ -5,7 +5,24 @@ All notable changes to this project will be documented in this file.
|
||||
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).
|
||||
|
||||
## Unreleased
|
||||
## Unreleased [3.0.0-alpha]
|
||||
|
||||
While this is unreleased, please only add v3 features here. Rebasing master onto v3 doesn't require a changelog update.
|
||||
|
||||
### Added
|
||||
|
||||
* `Scope` - An enum which contains all of the authorization scopes (see [here](https://github.com/plamere/spotipy/issues/652#issuecomment-797461311)).
|
||||
|
||||
### Changed
|
||||
|
||||
* Made `CacheHandler` an abstract base class
|
||||
|
||||
* Modified the return structure of the `audio_features` function (wrapping the [Get Audio Features for Several Tracks](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-audio-features) API) to conform to the return structure of the similar methods listed below. The functions wrapping these APIs do not unwrap the single key JSON response, and this is currently the only function that does this.
|
||||
* [Get Several Tracks](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-tracks)
|
||||
* [Get Multiple Artists](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-artists)
|
||||
* [Get Multiple Albums](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-albums)
|
||||
|
||||
## Unreleased [2.x.x]
|
||||
|
||||
### Added
|
||||
|
||||
@ -138,6 +155,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- `playlist_tracks` in favor of `playlist_items`
|
||||
|
||||
### Fixed
|
||||
|
||||
- fixed issue where episode URIs were being converted to track URIs in playlist calls
|
||||
|
||||
## [2.13.0] - 2020-06-25
|
||||
@ -246,6 +264,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Optional `show_dialog` parameter to be passed to `SpotifyOAuth`
|
||||
|
||||
### Changed
|
||||
|
||||
- Both `SpotifyClientCredentials` and `SpotifyOAuth` inherit from a common `SpotifyAuthBase` which handles common parameters and logics.
|
||||
|
||||
## [2.7.1] - 2020-01-20
|
||||
@ -289,16 +308,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [2.6.1] - 2020-01-13
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed inconsistent behaviour with some API methods when
|
||||
a full HTTP URL is passed.
|
||||
- Fixed invalid calls to logging warn method
|
||||
|
||||
### Removed
|
||||
|
||||
- `mock` no longer needed for install. Only used in `tox`.
|
||||
|
||||
## [2.6.0] - 2020-01-12
|
||||
|
||||
### Added
|
||||
|
||||
- Support for `playlist` to get a playlist without specifying a user
|
||||
- Support for `current_user_saved_albums_delete`
|
||||
- Support for `current_user_saved_albums_contains`
|
||||
@ -307,95 +329,126 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Lint with flake8 using Github action
|
||||
|
||||
### Changed
|
||||
|
||||
- Fix typos in doc
|
||||
- Start following [SemVer](https://semver.org) properly
|
||||
|
||||
## [2.5.0] - 2020-01-11
|
||||
|
||||
Added follow and player endpoints
|
||||
|
||||
## [2.4.4] - 2017-01-04
|
||||
|
||||
Python 3 fix
|
||||
|
||||
## [2.4.3] - 2017-01-02
|
||||
|
||||
Fixed proxy issue in standard auth flow
|
||||
|
||||
## [2.4.2] - 2017-01-02
|
||||
|
||||
Support getting audio features for a single track
|
||||
|
||||
## [2.4.1] - 2017-01-02
|
||||
|
||||
Incorporated proxy support
|
||||
|
||||
## [2.4.0] - 2016-12-31
|
||||
|
||||
Incorporated a number of PRs
|
||||
|
||||
## [2.3.8] - 2016-03-31
|
||||
|
||||
Added recs, audio features, user top lists
|
||||
|
||||
## [2.3.7] - 2015-08-10
|
||||
|
||||
Added current_user_followed_artists
|
||||
|
||||
## [2.3.6] - 2015-06-03
|
||||
|
||||
Support for offset/limit with album_tracks API
|
||||
|
||||
## [2.3.5] - 2015-04-28
|
||||
|
||||
Fixed bug in auto retry logic
|
||||
|
||||
## [2.3.3] - 2015-04-01
|
||||
|
||||
Aadded client credential flow
|
||||
|
||||
## [2.3.2] - 2015-03-31
|
||||
|
||||
Added auto retry logic
|
||||
|
||||
## [2.3.0] - 2015-01-05
|
||||
|
||||
Added session support added by akx.
|
||||
|
||||
## [2.2.0] - 2014-11-15
|
||||
|
||||
Added support for user_playlist_tracks
|
||||
|
||||
## [2.1.0] - 2014-10-25
|
||||
|
||||
Added support for new_releases and featured_playlists
|
||||
|
||||
## [2.0.2] - 2014-08-25
|
||||
|
||||
Moved to spotipy at pypi
|
||||
|
||||
## [1.2.0] - 2014-08-22
|
||||
|
||||
Upgraded APIs and docs to make it be a real library
|
||||
|
||||
## [1.310.0] - 2014-08-20
|
||||
|
||||
Added playlist replace and remove methods. Added auth tests. Improved API docs
|
||||
|
||||
## [1.301.0] - 2014-08-19
|
||||
|
||||
Upgraded version number to take precedence over previously botched release (sigh)
|
||||
|
||||
## [1.50.0] - 2014-08-14
|
||||
|
||||
Refactored util out of examples and into the main package
|
||||
|
||||
## [1.49.0] - 2014-07-23
|
||||
|
||||
Support for "Your Music" tracks (add, delete, get), with examples
|
||||
|
||||
## [1.45.0] - 2014-07-07
|
||||
|
||||
Support for related artists endpoint. Don't use cache auth codes when scope changes
|
||||
|
||||
## [1.44.0] - 2014-07-03
|
||||
|
||||
Added show tracks.py example
|
||||
|
||||
## [1.43.0] - 2014-06-27
|
||||
|
||||
Fixed JSON handling issue
|
||||
|
||||
## [1.42.0] - 2014-06-19
|
||||
|
||||
Removed dependency on simplejson
|
||||
|
||||
## [1.40.0] - 2014-06-12
|
||||
|
||||
Initial public release.
|
||||
|
||||
## [1.4.2] - 2014-06-21
|
||||
|
||||
Added support for retrieving starred playlists
|
||||
|
||||
## [1.1.0] - 2014-06-17
|
||||
|
||||
Updates to match released API
|
||||
|
||||
## [1.1.0] - 2014-05-18
|
||||
|
||||
Repackaged for saner imports
|
||||
|
||||
## [1.0.0] - 2017-04-05
|
||||
|
||||
Initial release
|
||||
2
setup.py
2
setup.py
@ -18,7 +18,7 @@ extra_reqs = {
|
||||
|
||||
setup(
|
||||
name='spotipy',
|
||||
version='2.18.0',
|
||||
version='3.0.0-alpha',
|
||||
description='A light weight Python library for the Spotify Web API',
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
|
||||
@ -3,3 +3,4 @@ from .client import * # noqa
|
||||
from .exceptions import * # noqa
|
||||
from .oauth2 import * # noqa
|
||||
from .util import * # noqa
|
||||
from .scope import * # noqa
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__all__ = ['CacheHandler', 'CacheFileHandler', 'MemoryCacheHandler']
|
||||
|
||||
import errno
|
||||
@ -5,33 +7,32 @@ import json
|
||||
import logging
|
||||
import os
|
||||
from spotipy.util import CLIENT_CREDS_ENV_VARS
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CacheHandler():
|
||||
class CacheHandler(ABC):
|
||||
"""
|
||||
An abstraction layer for handling the caching and retrieval of
|
||||
authorization tokens.
|
||||
|
||||
Custom extensions of this class must implement get_cached_token
|
||||
and save_token_to_cache methods with the same input and output
|
||||
structure as the CacheHandler class.
|
||||
Clients are expected to subclass this class and override the
|
||||
get_cached_token and save_token_to_cache methods with the same
|
||||
type signatures of this class.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_cached_token(self):
|
||||
"""
|
||||
Get and return a token_info dictionary object.
|
||||
"""
|
||||
# return token_info
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def save_token_to_cache(self, token_info):
|
||||
"""
|
||||
Save a token_info dictionary object to the cache and return None.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
return None
|
||||
|
||||
|
||||
class CacheFileHandler(CacheHandler):
|
||||
|
||||
@ -1221,7 +1221,6 @@ class Spotify(object):
|
||||
- limit - the number of tracks to return
|
||||
- offset - the index of the first track to return
|
||||
- market - an ISO 3166-1 alpha-2 country code
|
||||
|
||||
"""
|
||||
return self._get("me/tracks", limit=limit, offset=offset, market=market)
|
||||
|
||||
@ -1347,6 +1346,7 @@ class Spotify(object):
|
||||
Parameters:
|
||||
- shows - a list of show URIs, URLs or IDs
|
||||
"""
|
||||
|
||||
slist = [self._get_id("show", s) for s in shows]
|
||||
return self._get("me/shows/contains?ids=" + ",".join(slist))
|
||||
|
||||
@ -1675,11 +1675,6 @@ class Spotify(object):
|
||||
else:
|
||||
tlist = [self._get_id("track", t) for t in tracks]
|
||||
results = self._get("audio-features/?ids=" + ",".join(tlist))
|
||||
# the response has changed, look for the new style first, and if
|
||||
# its not there, fallback on the old style
|
||||
if "audio_features" in results:
|
||||
return results["audio_features"]
|
||||
else:
|
||||
return results
|
||||
|
||||
def devices(self):
|
||||
|
||||
@ -24,7 +24,10 @@ 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.util import CLIENT_CREDS_ENV_VARS, get_host_port, normalize_scope
|
||||
from spotipy.util import CLIENT_CREDS_ENV_VARS, get_host_port
|
||||
from spotipy.scope import Scope
|
||||
from typing import Iterable
|
||||
import re
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -72,6 +75,7 @@ def _ensure_value(value, env_key):
|
||||
|
||||
|
||||
class SpotifyAuthBase(object):
|
||||
|
||||
def __init__(self, requests_session):
|
||||
if isinstance(requests_session, requests.Session):
|
||||
self._session = requests_session
|
||||
@ -83,7 +87,40 @@ class SpotifyAuthBase(object):
|
||||
self._session = api
|
||||
|
||||
def _normalize_scope(self, scope):
|
||||
return normalize_scope(scope)
|
||||
"""
|
||||
Accepts a string of scopes, or an iterable with elements of type
|
||||
`Scope` or `str` and returns a space-separated string of scopes.
|
||||
Returns `None` if the argument is `None`.
|
||||
"""
|
||||
|
||||
# TODO: do we need to sort the scopes?
|
||||
|
||||
if isinstance(scope, str):
|
||||
# allow for any separator(s) between the scopes other than a word
|
||||
# character or a hyphen
|
||||
scopes = re.split(pattern=r"[^\w-]+", string=scope)
|
||||
return " ".join(sorted(scopes))
|
||||
|
||||
if isinstance(scope, Iterable):
|
||||
|
||||
# Assume all of the iterable's elements are of the same type.
|
||||
# If the iterable is empty, then return None.
|
||||
first_element = next(iter(scope), None)
|
||||
|
||||
if isinstance(first_element, str):
|
||||
return " ".join(sorted(scope))
|
||||
if isinstance(first_element, Scope):
|
||||
return Scope.make_string(scope)
|
||||
if first_element is None:
|
||||
return ""
|
||||
|
||||
elif scope is None:
|
||||
return None
|
||||
|
||||
raise TypeError(
|
||||
"Unsupported type for scopes: %s. Expected either a string of scopes, or "
|
||||
"an Iterable with elements of type `Scope` or `str`." % type(scope)
|
||||
)
|
||||
|
||||
@property
|
||||
def client_id(self):
|
||||
@ -289,7 +326,11 @@ class SpotifyOAuth(SpotifyAuthBase):
|
||||
* client_secret: Must be supplied or set as environment variable
|
||||
* redirect_uri: Must be supplied or set as environment variable
|
||||
* state: Optional, no verification is performed
|
||||
* scope: Optional, either a list of scopes or comma separated string of scopes.
|
||||
* scope: Optional, either a string of scopes, or an iterable with elements of type
|
||||
`Scope` or `str`. E.g.,
|
||||
{Scope.user_modify_playback_state, Scope.user_library_read}
|
||||
|
||||
iterable of scopes or comma separated string of scopes.
|
||||
e.g, "playlist-read-private,playlist-read-collaborative"
|
||||
* cache_path: (deprecated) Optional, will otherwise be generated
|
||||
(takes precedence over `username`)
|
||||
@ -650,8 +691,9 @@ class SpotifyPKCE(SpotifyAuthBase):
|
||||
* client_id: Must be supplied or set as environment variable
|
||||
* redirect_uri: Must be supplied or set as environment variable
|
||||
* state: Optional, no verification is performed
|
||||
* scope: Optional, either a list of scopes or comma separated string of scopes.
|
||||
e.g, "playlist-read-private,playlist-read-collaborative"
|
||||
* scope: Optional, either a string of scopes, or an iterable with elements of type
|
||||
`Scope` or `str`. E.g.,
|
||||
{Scope.user_modify_playback_state, Scope.user_library_read}
|
||||
* cache_path: (deprecated) Optional, will otherwise be generated
|
||||
(takes precedence over `username`)
|
||||
* username: (deprecated) Optional or set as environment variable
|
||||
@ -1050,8 +1092,9 @@ class SpotifyImplicitGrant(SpotifyAuthBase):
|
||||
* client_id: Must be supplied or set as environment variable
|
||||
* redirect_uri: Must be supplied or set as environment variable
|
||||
* state: May be supplied, no verification is performed
|
||||
* scope: Optional, either a list of scopes or comma separated string of scopes.
|
||||
e.g, "playlist-read-private,playlist-read-collaborative"
|
||||
* scope: Optional, either a string of scopes, or an iterable with elements of type
|
||||
`Scope` or `str`. E.g.,
|
||||
{Scope.user_modify_playback_state, Scope.user_library_read}
|
||||
* cache_handler: An instance of the `CacheHandler` class to handle
|
||||
getting and saving cached authorization tokens.
|
||||
May be supplied, will otherwise use `CacheFileHandler`.
|
||||
|
||||
85
spotipy/scope.py
Normal file
85
spotipy/scope.py
Normal file
@ -0,0 +1,85 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__all__ = ["Scope"]
|
||||
|
||||
from enum import Enum
|
||||
import re
|
||||
from typing import Iterable, Set
|
||||
|
||||
|
||||
class Scope(Enum):
|
||||
"""
|
||||
The Spotify authorization scopes
|
||||
|
||||
Create a Scope from a string:
|
||||
|
||||
scope = Scope("playlist-modify-private")
|
||||
|
||||
Create a set of scopes:
|
||||
|
||||
scopes = {
|
||||
Scope.user_read_currently_playing,
|
||||
Scope.playlist_read_collaborative,
|
||||
Scope.playlist_modify_public
|
||||
}
|
||||
"""
|
||||
|
||||
user_read_currently_playing = "user-read-currently-playing"
|
||||
playlist_read_collaborative = "playlist-read-collaborative"
|
||||
playlist_modify_private = "playlist-modify-private"
|
||||
user_read_playback_position = "user-read-playback-position"
|
||||
user_library_modify = "user-library-modify"
|
||||
user_top_read = "user-top-read"
|
||||
user_read_playback_state = "user-read-playback-state"
|
||||
user_read_email = "user-read-email"
|
||||
ugc_image_upload = "ugc-image-upload"
|
||||
user_read_private = "user-read-private"
|
||||
playlist_modify_public = "playlist-modify-public"
|
||||
user_library_read = "user-library-read"
|
||||
streaming = "streaming"
|
||||
user_read_recently_played = "user-read-recently-played"
|
||||
user_follow_read = "user-follow-read"
|
||||
user_follow_modify = "user-follow-modify"
|
||||
app_remote_control = "app-remote-control"
|
||||
playlist_read_private = "playlist-read-private"
|
||||
user_modify_playback_state = "user-modify-playback-state"
|
||||
|
||||
@staticmethod
|
||||
def all() -> Set['Scope']:
|
||||
"""Returns all of the authorization scopes"""
|
||||
|
||||
return set(Scope)
|
||||
|
||||
@staticmethod
|
||||
def make_string(scopes: Iterable['Scope']) -> str:
|
||||
"""
|
||||
Converts an iterable of scopes to a space-separated string.
|
||||
|
||||
* scopes: An iterable of scopes.
|
||||
|
||||
returns: a space-separated string of scopes
|
||||
"""
|
||||
return " ".join([scope.value for scope in scopes])
|
||||
|
||||
@staticmethod
|
||||
def from_string(scope_string: str) -> Set['Scope']:
|
||||
"""
|
||||
Converts a string of (usuallly space-separated) scopes into a
|
||||
set of scopes
|
||||
|
||||
Any scope-strings that do not match any of the known scopes are
|
||||
ignored.
|
||||
|
||||
* scope_string: a string of scopes
|
||||
|
||||
returns: a set of scopes.
|
||||
"""
|
||||
scope_string_list = re.split(pattern=r"[^\w-]+", string=scope_string)
|
||||
scopes = set()
|
||||
for scope_string in sorted(scope_string_list):
|
||||
try:
|
||||
scope = Scope(scope_string)
|
||||
scopes.add(scope)
|
||||
except ValueError:
|
||||
pass
|
||||
return scopes
|
||||
@ -7,7 +7,6 @@ __all__ = ["CLIENT_CREDS_ENV_VARS", "prompt_for_user_token"]
|
||||
import logging
|
||||
import os
|
||||
import warnings
|
||||
|
||||
import spotipy
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
@ -117,19 +116,3 @@ def get_host_port(netloc):
|
||||
port = None
|
||||
|
||||
return host, port
|
||||
|
||||
|
||||
def normalize_scope(scope):
|
||||
if scope:
|
||||
if isinstance(scope, str):
|
||||
scopes = scope.split(',')
|
||||
elif isinstance(scope, list) or isinstance(scope, tuple):
|
||||
scopes = scope
|
||||
else:
|
||||
raise Exception(
|
||||
"Unsupported scope value, please either provide a list of scopes, "
|
||||
"or a string of scopes separated by commas"
|
||||
)
|
||||
return " ".join(sorted(scopes))
|
||||
else:
|
||||
return None
|
||||
|
||||
@ -67,19 +67,19 @@ class AuthTestSpotipy(unittest.TestCase):
|
||||
|
||||
def test_audio_features(self):
|
||||
results = self.spotify.audio_features(self.four_tracks)
|
||||
self.assertTrue(len(results) == len(self.four_tracks))
|
||||
for track in results:
|
||||
self.assertTrue(len(results['audio_features']) == len(self.four_tracks))
|
||||
for track in results['audio_features']:
|
||||
assert('speechiness' in track)
|
||||
|
||||
def test_audio_features_with_bad_track(self):
|
||||
bad_tracks = ['spotify:track:bad']
|
||||
input = self.four_tracks + bad_tracks
|
||||
results = self.spotify.audio_features(input)
|
||||
self.assertTrue(len(results) == len(input))
|
||||
for track in results[:-1]:
|
||||
self.assertTrue(len(results['audio_features']) == len(input))
|
||||
for track in results['audio_features'][:-1]:
|
||||
if track is not None:
|
||||
assert('speechiness' in track)
|
||||
self.assertTrue(results[-1] is None)
|
||||
self.assertTrue(results['audio_features'][-1] is None)
|
||||
|
||||
def test_recommendations(self):
|
||||
results = self.spotify.recommendations(
|
||||
|
||||
90
tests/unit/test_scopes.py
Normal file
90
tests/unit/test_scopes.py
Normal file
@ -0,0 +1,90 @@
|
||||
from unittest import TestCase
|
||||
from spotipy.scope import Scope
|
||||
from spotipy.oauth2 import SpotifyAuthBase
|
||||
|
||||
|
||||
class SpotipyScopeTest(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.auth_manager = SpotifyAuthBase(requests_session=True)
|
||||
|
||||
def normalize_scope(self, scope):
|
||||
return self.auth_manager._normalize_scope(scope)
|
||||
|
||||
def test_empty_scope(self):
|
||||
scopes = set()
|
||||
scope_string = Scope.make_string(scopes)
|
||||
|
||||
normalized_scope_string = self.normalize_scope(scopes)
|
||||
normalized_scope_string_2 = self.normalize_scope(scope_string)
|
||||
|
||||
self.assertEqual(scope_string, "")
|
||||
self.assertEqual(normalized_scope_string, "")
|
||||
self.assertEqual(normalized_scope_string_2, "")
|
||||
|
||||
converted_scopes = Scope.from_string(scope_string)
|
||||
self.assertEqual(converted_scopes, set())
|
||||
|
||||
def test_scopes(self):
|
||||
scopes = {
|
||||
Scope.playlist_modify_public,
|
||||
Scope.playlist_read_collaborative,
|
||||
Scope.user_read_playback_state,
|
||||
Scope.ugc_image_upload
|
||||
}
|
||||
normalized_scope_string = self.normalize_scope(scopes)
|
||||
scope_string = Scope.make_string(scopes)
|
||||
self.assertEqual(scope_string, normalized_scope_string)
|
||||
|
||||
normalized_scope_string_2 = self.normalize_scope(scope_string)
|
||||
|
||||
converted_scopes = Scope.from_string(scope_string)
|
||||
normalized_converted_scope = Scope.from_string(normalized_scope_string)
|
||||
normalized_converted_scope_2 = Scope.from_string(normalized_scope_string_2)
|
||||
self.assertEqual(scopes, converted_scopes)
|
||||
self.assertEqual(scopes, normalized_converted_scope)
|
||||
self.assertEqual(scopes, normalized_converted_scope_2)
|
||||
|
||||
def test_single_scope(self):
|
||||
scope_string = "user-modify-playback-state"
|
||||
scope = Scope(scope_string)
|
||||
self.assertEqual(scope, Scope.user_modify_playback_state)
|
||||
self.assertEqual(scope_string, scope.value)
|
||||
|
||||
def test_scope_string(self):
|
||||
scope_string = (
|
||||
"user-read-currently-playing playlist-read-collaborative,user-library-read "
|
||||
"playlist-read-private user-read-email"
|
||||
)
|
||||
expected_scopes = {
|
||||
Scope.user_read_currently_playing,
|
||||
Scope.playlist_read_collaborative,
|
||||
Scope.user_library_read,
|
||||
Scope.playlist_read_private,
|
||||
Scope.user_read_email
|
||||
}
|
||||
parsed_scopes = Scope.from_string(scope_string)
|
||||
normalized_scope_string = self.normalize_scope(scope_string)
|
||||
normalized_parsed_scopes = Scope.from_string(normalized_scope_string)
|
||||
self.assertEqual(parsed_scopes, expected_scopes)
|
||||
self.assertEqual(normalized_parsed_scopes, expected_scopes)
|
||||
|
||||
def test_invalid_types(self):
|
||||
|
||||
numbers = [1, 2, 3]
|
||||
with self.assertRaises(TypeError):
|
||||
self.normalize_scope(numbers)
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
self.normalize_scope(True)
|
||||
|
||||
def test_normalize_scope(self):
|
||||
|
||||
normalized_scope_string = self.normalize_scope([])
|
||||
self.assertEqual(normalized_scope_string, "")
|
||||
|
||||
normalized_scope_string_2 = self.normalize_scope(())
|
||||
self.assertEqual(normalized_scope_string_2, "")
|
||||
|
||||
self.assertIsNone(self.normalize_scope(None))
|
||||
Loading…
Reference in New Issue
Block a user