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 }}
PYTHON_VERSION: "3.10"
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v1
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies

View File

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

View File

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

View File

@ -8,11 +8,11 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- 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 unused description parameter in playlist creation example
### Changed
- Drop support for EOL Python 3.7.
## [2.23.0] - 2023-04-07
### Added

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# spotipy documentation build configuration file, created by
# 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
Create virtual environment, install dependencies, run tests:::
$ virtualenv --python=python3.7 env
$ virtualenv --python=python3.12 env
(env) $ pip install --user -e .
(env) $ python -m unittest discover -v tests

View File

@ -1,6 +1,5 @@
# shows audio analysis for the given track
from __future__ import print_function # (at top of module)
from spotipy.oauth2 import SpotifyClientCredentials
import json
import spotipy
@ -20,4 +19,4 @@ start = time.time()
analysis = sp.audio_analysis(tid)
delta = time.time() - start
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
from __future__ import print_function # (at top of module)
from spotipy.oauth2 import SpotifyClientCredentials
import json
import spotipy
@ -33,4 +31,4 @@ for feature in features:
analysis = sp._get(feature['analysis_url'])
print(json.dumps(analysis, indent=4))
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
from __future__ import print_function # (at top of module)
from spotipy.oauth2 import SpotifyClientCredentials
import json
import spotipy
@ -22,4 +19,4 @@ if len(sys.argv) > 1:
features = sp.audio_features(tids)
delta = time.time() - start
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:
tid = sys.argv[1]
else:
print("Usage: %s track-id ..." % (sys.argv[0],))
print(f"Usage: {sys.argv[0]} track-id ...")
sys.exit()
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))

View File

@ -11,7 +11,7 @@ scope = 'user-library-modify'
if len(sys.argv) > 1:
tid = sys.argv[1]
else:
print("Usage: %s track-id ..." % (sys.argv[0],))
print(f"Usage: {sys.argv[0]} track-id ...")
sys.exit()
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)]})
else:
print(
"Usage: %s playlist_id track_id,pos track_id,pos ..." %
(sys.argv[0],))
f"Usage: {sys.argv[0]} playlist_id track_id,pos track_id,pos ...")
sys.exit()
scope = 'playlist-modify-public'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,5 +12,5 @@ class SpotifyException(Exception):
self.headers = headers
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)

View File

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

View File

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

View File

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

View File

@ -1,20 +1,15 @@
# -*- coding: utf-8 -*-
import io
import json
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.cache_handler import MemoryCacheHandler
from spotipy.oauth2 import SpotifyClientCredentials, SpotifyOauthError
from spotipy.oauth2 import SpotifyStateError
try:
import unittest.mock as mock
except ImportError:
import mock
patch = mock.patch
DEFAULT = mock.DEFAULT

View File

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