Skip to content

Commit 8877bef

Browse files
SG-25029 Add Python 3.9 coverage to Python API Azure Pipelines tests (#259)
* Map base 64 encode method * Fix configparser compatibility * Add tests config parser, string encoding and splitting urls * Fix assertEquals warnings
1 parent aca5fd7 commit 8877bef

File tree

9 files changed

+162
-51
lines changed

9 files changed

+162
-51
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,5 @@ htmlcov
3030
build
3131
dist
3232
shotgun_api3.egg-info
33+
/%1
34+

azure-pipelines-templates/run-tests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ jobs:
4848
python.version: '2.7'
4949
Python37:
5050
python.version: '3.7'
51+
Python39:
52+
python.version: '3.9'
5153

5254
# These are the steps that will be executed inside each job.
5355
steps:

shotgun_api3/lib/six.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
PY2 = sys.version_info[0] == 2
3737
PY3 = sys.version_info[0] == 3
3838
PY34 = sys.version_info[0:2] >= (3, 4)
39+
PY38 = sys.version_info[0:2] >= (3, 8)
3940

4041
if PY3:
4142
string_types = str,

shotgun_api3/shotgun.py

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
from .lib.six import BytesIO # used for attachment upload
3636
from .lib.six.moves import map
3737

38-
import base64
3938
from .lib.six.moves import http_cookiejar # used for attachment upload
4039
import datetime
4140
import logging
@@ -57,6 +56,12 @@
5756
# to be exposed as part of the API.
5857
from .lib.six.moves.xmlrpc_client import Error, ProtocolError, ResponseError # noqa
5958

59+
if six.PY3:
60+
from base64 import encodebytes as base64encode
61+
else:
62+
from base64 import encodestring as base64encode
63+
64+
6065
LOG = logging.getLogger("shotgun_api3")
6166
"""
6267
Logging instance for shotgun_api3
@@ -651,18 +656,20 @@ def __init__(self,
651656
# if the service contains user information strip it out
652657
# copied from the xmlrpclib which turned the user:password into
653658
# and auth header
654-
# Do NOT urlsplit(self.base_url) here, as it contains the lower case version
655-
# of the base_url argument. Doing so would base64-encode the lowercase
656-
# version of the credentials.
657-
auth, self.config.server = urllib.parse.splituser(urllib.parse.urlsplit(base_url).netloc)
659+
660+
# Do NOT self._split_url(self.base_url) here, as it contains the lower
661+
# case version of the base_url argument. Doing so would base64encode
662+
# the lowercase version of the credentials.
663+
auth, self.config.server = self._split_url(base_url)
658664
if auth:
659-
auth = base64.encodestring(six.ensure_binary(urllib.parse.unquote(auth))).decode("utf-8")
665+
auth = base64encode(six.ensure_binary(
666+
urllib.parse.unquote(auth))).decode("utf-8")
660667
self.config.authorization = "Basic " + auth.strip()
661668

662669
# foo:[email protected]:3456
663670
if http_proxy:
664-
# check if we're using authentication. Start from the end since there might be
665-
# @ in the user's password.
671+
# check if we're using authentication. Start from the end since
672+
# there might be @ in the user's password.
666673
p = http_proxy.rsplit("@", 1)
667674
if len(p) > 1:
668675
self.config.proxy_user, self.config.proxy_pass = \
@@ -710,6 +717,33 @@ def __init__(self,
710717
self.config.user_password = None
711718
self.config.auth_token = None
712719

720+
def _split_url(self, base_url):
721+
"""
722+
Extract the hostname:port and username/password/token from base_url
723+
sent when connect to the API.
724+
725+
In python 3.8 `urllib.parse.splituser` was deprecated warning devs to
726+
use `urllib.parse.urlparse`.
727+
"""
728+
if six.PY38:
729+
auth = None
730+
results = urllib.parse.urlparse(base_url)
731+
server = results.hostname
732+
if results.port:
733+
server = "{}:{}".format(server, results.port)
734+
735+
if results.username:
736+
auth = results.username
737+
738+
if results.password:
739+
auth = "{}:{}".format(auth, results.password)
740+
741+
else:
742+
auth, server = urllib.parse.splituser(
743+
urllib.parse.urlsplit(base_url).netloc)
744+
745+
return auth, server
746+
713747
# ========================================================================
714748
# API Functions
715749

tests/base.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
import os
33
import re
44
import unittest
5-
from shotgun_api3.lib.six.moves.configparser import ConfigParser
6-
75

86
from . import mock
97

@@ -12,6 +10,12 @@
1210
from shotgun_api3.shotgun import ServerCapabilities
1311
from shotgun_api3.lib import six
1412

13+
if six.PY2:
14+
from shotgun_api3.lib.six.moves.configparser import SafeConfigParser as ConfigParser
15+
else:
16+
from shotgun_api3.lib.six.moves.configparser import ConfigParser
17+
18+
1519
try:
1620
# Attempt to import skip from unittest. Since this was added in Python 2.7
1721
# in the case that we're running on Python 2.6 we'll need a decorator to

tests/test_api.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1661,7 +1661,7 @@ def test_zero_is_not_none(self):
16611661

16621662
# Should be filtered out
16631663
result = self.sg.find('Asset', [['id', 'is', self.asset['id']], [num_field, 'is_not', None]], [num_field])
1664-
self.assertEquals([], result)
1664+
self.assertEqual([], result)
16651665

16661666
# Set it to zero
16671667
self.sg.update('Asset', self.asset['id'], {num_field: 0})
@@ -1681,17 +1681,17 @@ def test_include_archived_projects(self):
16811681
if self.sg.server_caps.version > (5, 3, 13):
16821682
# Ticket #25082
16831683
result = self.sg.find_one('Shot', [['id', 'is', self.shot['id']]])
1684-
self.assertEquals(self.shot['id'], result['id'])
1684+
self.assertEqual(self.shot['id'], result['id'])
16851685

16861686
# archive project
16871687
self.sg.update('Project', self.project['id'], {'archived': True})
16881688

16891689
# setting defaults to True, so we should get result
16901690
result = self.sg.find_one('Shot', [['id', 'is', self.shot['id']]])
1691-
self.assertEquals(self.shot['id'], result['id'])
1691+
self.assertEqual(self.shot['id'], result['id'])
16921692

16931693
result = self.sg.find_one('Shot', [['id', 'is', self.shot['id']]], include_archived_projects=False)
1694-
self.assertEquals(None, result)
1694+
self.assertEqual(None, result)
16951695

16961696
# unarchive project
16971697
self.sg.update('Project', self.project['id'], {'archived': False})
@@ -2810,7 +2810,7 @@ def test_import_httplib(self):
28102810
# right one.)
28112811
httplib2_compat_version = httplib2.Http.__module__.split(".")[-1]
28122812
if six.PY2:
2813-
self.assertEquals(httplib2_compat_version, "python2")
2813+
self.assertEqual(httplib2_compat_version, "python2")
28142814
elif six.PY3:
28152815
self.assertTrue(httplib2_compat_version, "python3")
28162816

tests/test_client.py

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
CRUD functions. These tests always use a mock http connection so not not
1313
need a live server to run against."""
1414

15-
import base64
1615
import datetime
17-
from shotgun_api3.lib.six.moves import urllib
1816
import os
1917
import re
18+
19+
from shotgun_api3.lib.six.moves import urllib
2020
from shotgun_api3.lib import six
2121
try:
2222
import simplejson as json
@@ -38,8 +38,14 @@
3838
from . import base
3939

4040

41+
if six.PY3:
42+
from base64 import encodebytes as base64encode
43+
else:
44+
from base64 import encodestring as base64encode
45+
46+
4147
def b64encode(val):
42-
return base64.encodestring(six.ensure_binary(val)).decode("utf-8")
48+
return base64encode(six.ensure_binary(val)).decode("utf-8")
4349

4450

4551
class TestShotgunClient(base.MockTestBase):
@@ -164,6 +170,61 @@ def test_url(self):
164170
expected = "Basic " + b64encode(urllib.parse.unquote(login_password)).strip()
165171
self.assertEqual(expected, sg.config.authorization)
166172

173+
def test_b64encode(self):
174+
"""Parse value using the proper encoder."""
175+
login = "thelogin"
176+
password = "%thepassw0r#$"
177+
login_password = "%s:%s" % (login, password)
178+
expected = 'dGhlbG9naW46JXRoZXBhc3N3MHIjJA=='
179+
result = b64encode(urllib.parse.unquote(login_password)).strip()
180+
self.assertEqual(expected, result)
181+
182+
def test_read_config(self):
183+
"""Validate that config values are properly coerced."""
184+
this_dir = os.path.dirname(os.path.realpath(__file__))
185+
config_path = os.path.join(this_dir, "test_config_file")
186+
config = base.ConfigParser()
187+
config.read(config_path)
188+
result = config.get("SERVER_INFO", "api_key")
189+
expected = "%abce"
190+
191+
self.assertEqual(expected, result)
192+
193+
def test_split_url(self):
194+
"""Validate that url parts are properly extracted."""
195+
196+
sg = api.Shotgun("https://ci.shotgunstudio.com",
197+
"foo", "bar", connect=False)
198+
199+
200+
base_url = "https://ci.shotgunstudio.com"
201+
expected_server = "ci.shotgunstudio.com"
202+
expected_auth = None
203+
auth, server = sg._split_url(base_url)
204+
self.assertEqual(auth, expected_auth)
205+
self.assertEqual(server, expected_server)
206+
207+
base_url = "https://ci.shotgunstudio.com:9500"
208+
expected_server = "ci.shotgunstudio.com:9500"
209+
expected_auth = None
210+
auth, server = sg._split_url(base_url)
211+
self.assertEqual(auth, expected_auth)
212+
self.assertEqual(server, expected_server)
213+
214+
base_url = "https://x:[email protected]:9500"
215+
expected_server = "ci.shotgunstudio.com:9500"
216+
expected_auth = "x:y"
217+
auth, server = sg._split_url(base_url)
218+
self.assertEqual(auth, expected_auth)
219+
self.assertEqual(server, expected_server)
220+
221+
base_url = "https://[email protected]:9500"
222+
expected_server = "ci.shotgunstudio.com:9500"
223+
expected_auth = "12345XYZ"
224+
auth, server = sg._split_url(base_url)
225+
self.assertEqual(auth, expected_auth)
226+
self.assertEqual(server, expected_server)
227+
167228
def test_authorization(self):
168229
"""Authorization passed to server"""
169230
login = self.human_user['login']

tests/test_config_file

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[SERVER_INFO]
2+
server_url : https://url
3+
script_name : xyz
4+
api_key : %%abce
5+
6+
[TEST_DATA]
7+
project_name : hjkl

0 commit comments

Comments
 (0)