From 999d92dde25f24727b30fb724e0b02ecfafafc58 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 16 Jul 2025 10:16:45 +0200 Subject: [PATCH 01/17] Import order --- shotgun_api3/shotgun.py | 24 ++++++++++++------------ tests/test_api.py | 18 ++++++++++-------- tests/test_client.py | 8 ++++---- tests/test_unit.py | 1 + 4 files changed, 27 insertions(+), 24 deletions(-) diff --git a/shotgun_api3/shotgun.py b/shotgun_api3/shotgun.py index fea25deba..626fedcee 100644 --- a/shotgun_api3/shotgun.py +++ b/shotgun_api3/shotgun.py @@ -29,27 +29,27 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ -# Python 2/3 compatibility -from .lib import six -from .lib import sgsix -from .lib import sgutils -from .lib.six import BytesIO # used for attachment upload -from .lib.six.moves import map - -from .lib.six.moves import http_cookiejar # used for attachment upload +import copy import datetime +import json import logging -import uuid # used for attachment upload import os import re -import copy +import shutil # used for attachment download import ssl import stat # used for attachment upload import sys import time -import json +import uuid # used for attachment upload + +# Python 2/3 compatibility +from .lib import six +from .lib import sgsix +from .lib import sgutils +from .lib.six import BytesIO # used for attachment upload +from .lib.six.moves import map +from .lib.six.moves import http_cookiejar # used for attachment upload from .lib.six.moves import urllib -import shutil # used for attachment download from .lib.six.moves import http_client # Used for secure file upload. from .lib.httplib2 import Http, ProxyInfo, socks, ssl_error_classes from .lib.sgtimezone import SgTimezone diff --git a/tests/test_api.py b/tests/test_api.py index 788c9751b..ef7f3bc84 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -16,29 +16,31 @@ from __future__ import print_function import datetime -import sys +import glob import os -from . import mock -from .mock import patch, MagicMock import ssl +import sys import time import types -import uuid import unittest -from shotgun_api3.lib.six.moves import range, urllib +import uuid import warnings -import glob -import shotgun_api3 -from shotgun_api3.lib.httplib2 import Http from shotgun_api3.lib import six +from shotgun_api3.lib.httplib2 import Http # To mock the correct exception when testion on Python 2 and 3, use the # ShotgunSSLError variable from sgsix that contains the appropriate exception # class for the current Python version. from shotgun_api3.lib.sgsix import ShotgunSSLError +from shotgun_api3.lib.six.moves import range, urllib + +import shotgun_api3 + from . import base +from . import mock +from .mock import patch, MagicMock class TestShotgunApi(base.LiveTestBase): diff --git a/tests/test_client.py b/tests/test_client.py index 4a81996f1..b6e5e39bb 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -14,7 +14,11 @@ import datetime import os +import platform import re +import sys +import time +import unittest from shotgun_api3.lib.six.moves import urllib from shotgun_api3.lib import six, sgutils @@ -27,10 +31,6 @@ except ImportError: import shotgun_api3.lib.simplejson as json -import platform -import sys -import time -import unittest from . import mock import shotgun_api3.lib.httplib2 as httplib2 diff --git a/tests/test_unit.py b/tests/test_unit.py index de996c553..ff78253c2 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -13,6 +13,7 @@ import os import unittest from unittest import mock + from .mock import patch import shotgun_api3 as api from shotgun_api3.shotgun import _is_mimetypes_broken From 9777db084671aee6b64205d4dec3eee10deee7ea Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 16 Jul 2025 10:02:34 +0200 Subject: [PATCH 02/17] Remove __future__ imports --- tests/test_api.py | 1 - tests/test_api_long.py | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index ef7f3bc84..83cf1ef7e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -14,7 +14,6 @@ test_api_long for other tests. """ -from __future__ import print_function import datetime import glob import os diff --git a/tests/test_api_long.py b/tests/test_api_long.py index 0bf509b3c..9425012bd 100644 --- a/tests/test_api_long.py +++ b/tests/test_api_long.py @@ -13,7 +13,6 @@ Includes the schema functions and the automated searching for all entity types """ -from __future__ import print_function from . import base import random import shotgun_api3 From e3e72cdbe8529ea53f0f724ef00905a0e6185b34 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 16 Jul 2025 10:05:30 +0200 Subject: [PATCH 03/17] Cleanup super prototype --- tests/base.py | 8 ++++---- tests/test_api.py | 24 ++++++++++++------------ tests/test_client.py | 2 +- tests/test_mockgun.py | 4 ++-- tests/test_proxy.py | 2 +- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/base.py b/tests/base.py index 2820d495d..cc8634996 100644 --- a/tests/base.py +++ b/tests/base.py @@ -135,7 +135,7 @@ class MockTestBase(TestBase): """Test base for tests mocking server interactions.""" def setUp(self): - super(MockTestBase, self).setUp() + super().setUp() # TODO see if there is another way to stop sg connecting self._setup_mock() self._setup_mock_data() @@ -252,7 +252,7 @@ class LiveTestBase(TestBase): def setUp(self, auth_mode=None): if not auth_mode: auth_mode = "HumanUser" if self.config.jenkins else "ApiUser" - super(LiveTestBase, self).setUp(auth_mode) + super().setUp(auth_mode) if ( self.sg.server_caps.version and self.sg.server_caps.version >= (3, 3, 0) @@ -410,7 +410,7 @@ class HumanUserAuthLiveTestBase(LiveTestBase): """ def setUp(self): - super(HumanUserAuthLiveTestBase, self).setUp("HumanUser") + super().setUp("HumanUser") class SessionTokenAuthLiveTestBase(LiveTestBase): @@ -420,7 +420,7 @@ class SessionTokenAuthLiveTestBase(LiveTestBase): """ def setUp(self): - super(SessionTokenAuthLiveTestBase, self).setUp("SessionToken") + super().setUp("SessionToken") class SgTestConfig(object): diff --git a/tests/test_api.py b/tests/test_api.py index 83cf1ef7e..ccc9e544a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -44,7 +44,7 @@ class TestShotgunApi(base.LiveTestBase): def setUp(self): - super(TestShotgunApi, self).setUp() + super().setUp() # give note unicode content self.sg.update("Note", self.note["id"], {"content": "La Pe\xf1a"}) @@ -1060,7 +1060,7 @@ class TestDataTypes(base.LiveTestBase): """ def setUp(self): - super(TestDataTypes, self).setUp() + super().setUp() def test_set_checkbox(self): entity = "HumanUser" @@ -1270,7 +1270,7 @@ class TestUtc(base.LiveTestBase): """Test utc options""" def setUp(self): - super(TestUtc, self).setUp() + super().setUp() utc = shotgun_api3.shotgun.SG_TIMEZONE.utc self.datetime_utc = datetime.datetime(2008, 10, 13, 23, 10, tzinfo=utc) local = shotgun_api3.shotgun.SG_TIMEZONE.local @@ -1312,7 +1312,7 @@ def _assert_expected(self, sg, date_time, expected): class TestFind(base.LiveTestBase): def setUp(self): - super(TestFind, self).setUp() + super().setUp() # We will need the created_at field for the shot fields = list(self.shot.keys())[:] fields.append("created_at") @@ -2108,7 +2108,7 @@ def test_following(self): class TestErrors(base.TestBase): def setUp(self): auth_mode = "HumanUser" if self.config.jenkins else "ApiUser" - super(TestErrors, self).setUp(auth_mode) + super().setUp(auth_mode) def test_bad_auth(self): """test_bad_auth invalid script name or api key raises fault""" @@ -2434,7 +2434,7 @@ def test_upload_missing_file(self): class TestScriptUserSudoAuth(base.LiveTestBase): def setUp(self): - super(TestScriptUserSudoAuth, self).setUp() + super().setUp() self.sg.update( "HumanUser", @@ -2475,7 +2475,7 @@ def test_user_is_creator(self): class TestHumanUserSudoAuth(base.TestBase): def setUp(self): - super(TestHumanUserSudoAuth, self).setUp("HumanUser") + super().setUp("HumanUser") def test_human_user_sudo_auth_fails(self): """ @@ -2746,7 +2746,7 @@ class TestActivityStream(base.LiveTestBase): """ def setUp(self): - super(TestActivityStream, self).setUp() + super().setUp() self._prefix = uuid.uuid4().hex self._shot = self.sg.create( @@ -2796,7 +2796,7 @@ def tearDown(self): ) self.sg.batch(batch_data) - super(TestActivityStream, self).tearDown() + super().tearDown() def test_simple(self): """ @@ -2869,7 +2869,7 @@ class TestNoteThreadRead(base.LiveTestBase): """ def setUp(self): - super(TestNoteThreadRead, self).setUp() + super().setUp() # get path to our std attahcment this_dir, _ = os.path.split(__file__) @@ -3080,7 +3080,7 @@ class TestTextSearch(base.LiveTestBase): """ def setUp(self): - super(TestTextSearch, self).setUp() + super().setUp() # create 5 shots and 5 assets to search for self._prefix = uuid.uuid4().hex @@ -3120,7 +3120,7 @@ def tearDown(self): ) self.sg.batch(batch_data) - super(TestTextSearch, self).tearDown() + super().tearDown() def test_simple(self): """ diff --git a/tests/test_client.py b/tests/test_client.py index b6e5e39bb..9b3e6ba18 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -52,7 +52,7 @@ class TestShotgunClient(base.MockTestBase): """Test case for shotgun api with server interactions mocked.""" def setUp(self): - super(TestShotgunClient, self).setUp() + super().setUp() # get domain and uri scheme match = re.search("(https?://)(.*)", self.server_url) self.uri_prefix = match.group(1) diff --git a/tests/test_mockgun.py b/tests/test_mockgun.py index e7e4295e4..ad478304a 100644 --- a/tests/test_mockgun.py +++ b/tests/test_mockgun.py @@ -79,7 +79,7 @@ def setUp(self): """ Creates test data. """ - super(TestValidateFilterSyntax, self).setUp() + super().setUp() self._mockgun = Mockgun( "https://test.shotgunstudio.com", login="user", password="1234" @@ -578,7 +578,7 @@ def setUp(self): """ Creates tests data. """ - super(TestFilterOperator, self).setUp() + super().setUp() self._mockgun = Mockgun( "https://test.shotgunstudio.com", login="user", password="1234" diff --git a/tests/test_proxy.py b/tests/test_proxy.py index cb713cd9d..7bf0d7006 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -18,7 +18,7 @@ class ServerConnectionTest(base.TestBase): """Tests for server connection""" def setUp(self): - super(ServerConnectionTest, self).setUp() + super().setUp() def test_connection(self): """Tests server connects and returns nothing""" From 2e3b54ba479fea84a6c0aaa86bf20832e92352d6 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 16 Jul 2025 12:17:27 +0200 Subject: [PATCH 04/17] six.iter.... --- shotgun_api3/shotgun.py | 17 ++++++++--------- tests/test_api_long.py | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/shotgun_api3/shotgun.py b/shotgun_api3/shotgun.py index 626fedcee..a192bea5f 100644 --- a/shotgun_api3/shotgun.py +++ b/shotgun_api3/shotgun.py @@ -2270,8 +2270,7 @@ def schema_field_update( "type": entity_type, "field_name": field_name, "properties": [ - {"property_name": k, "value": v} - for k, v in six.iteritems((properties or {})) + {"property_name": k, "value": v} for k, v in (properties or {}).items() ], } params = self._add_project_param(params, project_entity) @@ -3328,7 +3327,7 @@ def text_search(self, text, entity_types, project_ids=None, limit=None): raise ValueError("entity_types parameter must be a dictionary") api_entity_types = {} - for entity_type, filter_list in six.iteritems(entity_types): + for entity_type, filter_list in entity_types.items(): if isinstance(filter_list, (list, tuple)): resolved_filters = _translate_filters(filter_list, filter_operator=None) @@ -3965,7 +3964,7 @@ def _http_request(self, verb, path, body, headers): resp, content = conn.request(url, method=verb, body=body, headers=headers) # http response code is handled else where http_status = (resp.status, resp.reason) - resp_headers = dict((k.lower(), v) for k, v in six.iteritems(resp)) + resp_headers = dict((k.lower(), v) for k, v in resp.items()) resp_body = content LOG.debug("Response status is %s %s" % http_status) @@ -4045,7 +4044,7 @@ def _decode_list(lst): def _decode_dict(dct): newdict = {} - for k, v in six.iteritems(dct): + for k, v in dct.items(): if isinstance(k, str): k = sgutils.ensure_str(k) if isinstance(v, str): @@ -4119,7 +4118,7 @@ def _visit_data(self, data, visitor): return tuple(recursive(i, visitor) for i in data) if isinstance(data, dict): - return dict((k, recursive(v, visitor)) for k, v in six.iteritems(data)) + return dict((k, recursive(v, visitor)) for k, v in data.items()) return visitor(data) @@ -4288,7 +4287,7 @@ def _parse_records(self, records): continue # iterate over each item and check each field for possible injection - for k, v in six.iteritems(rec): + for k, v in rec.items(): if not v: continue @@ -4376,7 +4375,7 @@ def _dict_to_list( [{'field_name': 'foo', 'value': 'bar', 'thing1': 'value1'}] """ ret = [] - for k, v in six.iteritems((d or {})): + for k, v in (d or {}).items(): d = {key_name: k, value_name: v} d.update((extra_data or {}).get(k, {})) ret.append(d) @@ -4389,7 +4388,7 @@ def _dict_to_extra_data(self, d, key_name="value"): e.g. d {'foo' : 'bar'} changed to {'foo': {"value": 'bar'}] """ - return dict([(k, {key_name: v}) for (k, v) in six.iteritems((d or {}))]) + return dict([(k, {key_name: v}) for (k, v) in (d or {}).items()]) def _upload_file_to_storage(self, path, storage_url): """ diff --git a/tests/test_api_long.py b/tests/test_api_long.py index 9425012bd..29a34e991 100644 --- a/tests/test_api_long.py +++ b/tests/test_api_long.py @@ -55,7 +55,7 @@ def test_automated_find(self): # pivot_column fields aren't valid for sorting so ensure we're # not using one. order_field = None - for field_name, field in six.iteritems(fields): + for field_name, field in fields.items(): # Restrict sorting to only types we know will always be sortable # Since no_sorting is not exposed to us, we'll have to rely on # this as a safeguard against trying to sort by a field with From 64fa354a7421e3d5cd9811dfa5b9fb25264526d1 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 16 Jul 2025 12:21:26 +0200 Subject: [PATCH 05/17] Remove calls to six.text_type and six.binary_type --- tests/test_api.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index ccc9e544a..751ace799 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -246,9 +246,7 @@ def test_upload_download(self): # test upload of non-ascii, unicode path u_path = os.path.abspath( - os.path.expanduser( - glob.glob(os.path.join(six.text_type(this_dir), "Noëlご.jpg"))[0] - ) + os.path.expanduser(glob.glob(os.path.join(this_dir, "Noëlご.jpg"))[0]) ) # If this is a problem, it'll raise with a UnicodeEncodeError. We @@ -326,9 +324,7 @@ def test_upload_to_sg(self, mock_send_form): mock_send_form.return_value = "1\n:123\nasd" this_dir, _ = os.path.split(__file__) u_path = os.path.abspath( - os.path.expanduser( - glob.glob(os.path.join(six.text_type(this_dir), "Noëlご.jpg"))[0] - ) + os.path.expanduser(glob.glob(os.path.join(this_dir, "Noëlご.jpg"))[0]) ) upload_id = self.sg.upload( "Version", @@ -418,7 +414,7 @@ def test_upload_thumbnail_in_create(self): url = new_version.get("filmstrip_image") data = self.sg.download_attachment({"url": url}) - self.assertTrue(isinstance(data, six.binary_type)) + self.assertTrue(isinstance(data, bytes)) self.sg.delete("Version", new_version["id"]) @@ -3504,9 +3500,9 @@ def test_import_httplib(self): def _has_unicode(data): for k, v in data.items(): - if isinstance(k, six.text_type): + if isinstance(k, str): return True - if isinstance(v, six.text_type): + if isinstance(v, str): return True return False From d014745c8a05f8e4a428c7bf4874d77d35931928 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 16 Jul 2025 12:01:50 +0200 Subject: [PATCH 06/17] Remove calls to ensure_bytes, ensure_text, ensure_strings --- docs/reference.rst | 3 -- shotgun_api3/shotgun.py | 63 +++++++++++++++++++++++------------------ tests/test_client.py | 13 +++++---- 3 files changed, 42 insertions(+), 37 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index e2e050e86..96c917469 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1034,6 +1034,3 @@ Example for a user whose language preference is set to Japanese: }, ... } - -.. note:: - If needed, the encoding of the returned localized string can be ensured regardless the Python version using shotgun_api3.lib.six.ensure_text(). diff --git a/shotgun_api3/shotgun.py b/shotgun_api3/shotgun.py index a192bea5f..7b8bee794 100644 --- a/shotgun_api3/shotgun.py +++ b/shotgun_api3/shotgun.py @@ -713,7 +713,7 @@ def __init__( auth, self.config.server = self._split_url(base_url) if auth: auth = base64encode( - sgutils.ensure_binary(urllib.parse.unquote(auth)) + urllib.parse.unquote(auth).encode("utf-8") ).decode("utf-8") self.config.authorization = "Basic " + auth.strip() @@ -2965,7 +2965,11 @@ def download_attachment(self, attachment=False, file_path=None, attachment_id=No url.find("s3.amazonaws.com") != -1 and e.headers["content-type"] == "application/xml" ): - body = [sgutils.ensure_text(line) for line in e.readlines()] + body = [ + line.decode("utf-8") if isinstance(line, bytes) else line + for line in e.readlines() + ] + if body: xml = "".join(body) # Once python 2.4 support is not needed we can think about using @@ -3858,8 +3862,7 @@ def _encode_payload(self, payload): be in a single byte encoding to go over the wire. """ - wire = json.dumps(payload, ensure_ascii=False) - return sgutils.ensure_binary(wire) + return json.dumps(payload, ensure_ascii=False).encode("utf-8") def _make_call(self, verb, path, body, headers): """ @@ -4165,10 +4168,6 @@ def _outbound_visitor(value): value = _change_tz(value) return value.strftime("%Y-%m-%dT%H:%M:%SZ") - # ensure return is six.text_type - if isinstance(value, str): - return sgutils.ensure_text(value) - return value return self._visit_data(data, _outbound_visitor) @@ -4656,7 +4655,10 @@ def _send_form(self, url, params): else: raise ShotgunError("Unanticipated error occurred %s" % (e)) - return sgutils.ensure_text(result) + if isinstance(result, bytes): + result = result.decode("utf-8") + + return result else: raise ShotgunError("Max attemps limit reached.") @@ -4737,9 +4739,8 @@ def http_request(self, request): else: params.append((key, value)) if not files: - data = sgutils.ensure_binary( - urllib.parse.urlencode(params, True) - ) # sequencing on + data = urllib.parse.urlencode(params, True).encode("utf-8") + # sequencing on else: boundary, data = self.encode(params, files) content_type = "multipart/form-data; boundary=%s" % boundary @@ -4762,42 +4763,48 @@ def encode(self, params, files, boundary=None, buffer=None): if buffer is None: buffer = BytesIO() for key, value in params: - if not isinstance(value, str): + if isinstance(key, bytes): + key = key.decode("utf-8") + + if isinstance(value, bytes): + value = value.decode("utf-8") + elif not isinstance(value, str): # If value is not a string (e.g. int) cast to text value = str(value) - value = sgutils.ensure_text(value) - key = sgutils.ensure_text(key) - buffer.write(sgutils.ensure_binary("--%s\r\n" % boundary)) + buffer.write(f"--{boundary}\r\n".encode("utf-8")) buffer.write( - sgutils.ensure_binary('Content-Disposition: form-data; name="%s"' % key) + f'Content-Disposition: form-data; name="{key}"'.encode("utf-8") ) - buffer.write(sgutils.ensure_binary("\r\n\r\n%s\r\n" % value)) + buffer.write(f"\r\n\r\n{value}\r\n".encode("utf-8")) for key, fd in files: # On Windows, it's possible that we were forced to open a file # with non-ascii characters as unicode. In that case, we need to # encode it as a utf-8 string to remove unicode from the equation. # If we don't, the mix of unicode and strings going into the # buffer can cause UnicodeEncodeErrors to be raised. - filename = fd.name - filename = sgutils.ensure_text(filename) + filename = ( + fd.name.decode("utf-8") if isinstance(fd.name, bytes) else fd.name + ) filename = filename.split("/")[-1] - key = sgutils.ensure_text(key) + if isinstance(key, bytes): + key = key.decode("utf-8") + content_type = mimetypes.guess_type(filename)[0] content_type = content_type or "application/octet-stream" file_size = os.fstat(fd.fileno())[stat.ST_SIZE] - buffer.write(sgutils.ensure_binary("--%s\r\n" % boundary)) + buffer.write(f"--{boundary}\r\n".encode("utf-8")) c_dis = 'Content-Disposition: form-data; name="%s"; filename="%s"%s' content_disposition = c_dis % (key, filename, "\r\n") - buffer.write(sgutils.ensure_binary(content_disposition)) - buffer.write(sgutils.ensure_binary("Content-Type: %s\r\n" % content_type)) - buffer.write(sgutils.ensure_binary("Content-Length: %s\r\n" % file_size)) + buffer.write(content_disposition.encode("utf-8")) + buffer.write(f"Content-Type: {content_type}\r\n".encode("utf-8")) + buffer.write(f"Content-Length: {file_size}\r\n".encode("utf-8")) - buffer.write(sgutils.ensure_binary("\r\n")) + buffer.write(b"\r\n") fd.seek(0) shutil.copyfileobj(fd, buffer) - buffer.write(sgutils.ensure_binary("\r\n")) - buffer.write(sgutils.ensure_binary("--%s--\r\n\r\n" % boundary)) + buffer.write(b"\r\n") + buffer.write("--{boundary}--\r\n\r\n".encode("utf-8")) buffer = buffer.getvalue() return boundary, buffer diff --git a/tests/test_client.py b/tests/test_client.py index 9b3e6ba18..0c62fb9c5 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -45,7 +45,10 @@ def b64encode(val): - return base64encode(sgutils.ensure_binary(val)).decode("utf-8") + if isinstance(val, str): + val = val.encode("utf-8") + + return base64encode(val).decode("utf-8") class TestShotgunClient(base.MockTestBase): @@ -433,8 +436,8 @@ def test_call_rpc(self): # Test unicode mixed with utf-8 as reported in Ticket #17959 d = {"results": ["foo", "bar"]} a = { - "utf_str": "\xe2\x88\x9a", - "unicode_str": sgutils.ensure_text("\xe2\x88\x9a"), + "utf_str": b"\xe2\x88\x9a", + "unicode_str": "\xe2\x88\x9a", } self._mock_http(d) rv = self.sg._call_rpc("list", a) @@ -648,9 +651,7 @@ def test_encode_payload(self): self.assertTrue(isinstance(j, bytes)) def test_decode_response_ascii(self): - self._assert_decode_resonse( - True, sgutils.ensure_str("my data \u00e0", encoding="utf8") - ) + self._assert_decode_resonse(True, "my data \u00e0") def test_decode_response_unicode(self): self._assert_decode_resonse(False, "my data \u00e0") From 153f17966b33fda41cfb7d735aed55ea0082f488 Mon Sep 17 00:00:00 2001 From: Eduardo Chauca Date: Thu, 17 Jul 2025 00:27:08 -0500 Subject: [PATCH 07/17] test fixed --- tests/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_client.py b/tests/test_client.py index 0c62fb9c5..11b33acc3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -436,7 +436,7 @@ def test_call_rpc(self): # Test unicode mixed with utf-8 as reported in Ticket #17959 d = {"results": ["foo", "bar"]} a = { - "utf_str": b"\xe2\x88\x9a", + "utf_str": "\xe2\x88\x9a", "unicode_str": "\xe2\x88\x9a", } self._mock_http(d) From 2de28220319f4fe57a3db64186ccd7f7ebf63a1a Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 17 Jul 2025 14:43:40 +0200 Subject: [PATCH 08/17] fixup! test fixed --- tests/test_client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 11b33acc3..9cb50a168 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -433,11 +433,10 @@ def test_call_rpc(self): expected = "rpc response with list result, first item" self.assertEqual(d["results"][0], rv, expected) - # Test unicode mixed with utf-8 as reported in Ticket #17959 + # Test payload encoding with non-ascii characters (using utf-8 literal) d = {"results": ["foo", "bar"]} a = { - "utf_str": "\xe2\x88\x9a", - "unicode_str": "\xe2\x88\x9a", + "utf_literal": "\xe2\x88\x9a", } self._mock_http(d) rv = self.sg._call_rpc("list", a) From 24835e35e9337e53de8abaf6b0e8d9f617935268 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 17 Jul 2025 15:32:52 +0200 Subject: [PATCH 09/17] Black --- shotgun_api3/shotgun.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shotgun_api3/shotgun.py b/shotgun_api3/shotgun.py index 7b8bee794..6aaaeead0 100644 --- a/shotgun_api3/shotgun.py +++ b/shotgun_api3/shotgun.py @@ -712,9 +712,9 @@ def __init__( # the lowercase version of the credentials. auth, self.config.server = self._split_url(base_url) if auth: - auth = base64encode( - urllib.parse.unquote(auth).encode("utf-8") - ).decode("utf-8") + auth = base64encode(urllib.parse.unquote(auth).encode("utf-8")).decode( + "utf-8" + ) self.config.authorization = "Basic " + auth.strip() # foo:bar@123.456.789.012:3456 From 60bb0bc996ccc6cc0aa8c7d6d6e34752ae729dac Mon Sep 17 00:00:00 2001 From: Julien Langlois <16244608+julien-lang@users.noreply.github.com> Date: Thu, 17 Jul 2025 07:01:38 -0700 Subject: [PATCH 10/17] Update shotgun_api3/shotgun.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- shotgun_api3/shotgun.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shotgun_api3/shotgun.py b/shotgun_api3/shotgun.py index 6aaaeead0..2bede5da1 100644 --- a/shotgun_api3/shotgun.py +++ b/shotgun_api3/shotgun.py @@ -4804,7 +4804,7 @@ def encode(self, params, files, boundary=None, buffer=None): fd.seek(0) shutil.copyfileobj(fd, buffer) buffer.write(b"\r\n") - buffer.write("--{boundary}--\r\n\r\n".encode("utf-8")) + buffer.write(f"--{boundary}--\r\n\r\n".encode("utf-8")) buffer = buffer.getvalue() return boundary, buffer From 473b81103e6238b382a61cba18ac683a65ae5b5c Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 16 Jul 2025 10:29:01 +0200 Subject: [PATCH 11/17] six.moves imports --- docs/cookbook/examples/ami_handler.rst | 2 +- shotgun_api3/lib/mockgun/schema.py | 2 +- shotgun_api3/shotgun.py | 25 +++++++++++++------------ tests/base.py | 6 +++--- tests/test_api.py | 5 +++-- tests/test_client.py | 3 ++- tests/test_unit.py | 3 ++- 7 files changed, 25 insertions(+), 21 deletions(-) diff --git a/docs/cookbook/examples/ami_handler.rst b/docs/cookbook/examples/ami_handler.rst index 3fb5e3571..6b8f3384b 100644 --- a/docs/cookbook/examples/ami_handler.rst +++ b/docs/cookbook/examples/ami_handler.rst @@ -218,7 +218,7 @@ via ``POST``. If you're using a custom protocol the data is sent via ``GET``. params = params.split("&") p = {"column_display_names": [], "cols": []} for arg in params: - key, value = map(six.moves.urllib.parse.unquote, arg.split("=", 1)) + key, value = map(urllib.parse.unquote, arg.split("=", 1)) if key == "column_display_names" or key == "cols": p[key].append(value) else: diff --git a/shotgun_api3/lib/mockgun/schema.py b/shotgun_api3/lib/mockgun/schema.py index 5d5019df4..ab671629d 100644 --- a/shotgun_api3/lib/mockgun/schema.py +++ b/shotgun_api3/lib/mockgun/schema.py @@ -30,8 +30,8 @@ ----------------------------------------------------------------------------- """ -from ..six.moves import cPickle as pickle import os +import pickle from .errors import MockgunError diff --git a/shotgun_api3/shotgun.py b/shotgun_api3/shotgun.py index 2bede5da1..338413810 100644 --- a/shotgun_api3/shotgun.py +++ b/shotgun_api3/shotgun.py @@ -32,6 +32,8 @@ import copy import datetime import json +import http.client # Used for secure file upload +import http.cookiejar # used for attachment upload import logging import os import re @@ -40,24 +42,23 @@ import stat # used for attachment upload import sys import time +import urllib.error +import urllib.parse +import urllib.request import uuid # used for attachment upload +# Import Error and ResponseError (even though they're unused in this file) since they need +# to be exposed as part of the API. +from xmlrpc.client import Error, ProtocolError, ResponseError # noqa + # Python 2/3 compatibility from .lib import six from .lib import sgsix from .lib import sgutils from .lib.six import BytesIO # used for attachment upload -from .lib.six.moves import map -from .lib.six.moves import http_cookiejar # used for attachment upload -from .lib.six.moves import urllib -from .lib.six.moves import http_client # Used for secure file upload. from .lib.httplib2 import Http, ProxyInfo, socks, ssl_error_classes from .lib.sgtimezone import SgTimezone -# Import Error and ResponseError (even though they're unused in this file) since they need -# to be exposed as part of the API. -from .lib.six.moves.xmlrpc_client import Error, ProtocolError, ResponseError # noqa - if six.PY3: from base64 import encodebytes as base64encode else: @@ -3003,8 +3004,8 @@ def get_auth_cookie_handler(self): This is used internally for downloading attachments from FPTR. """ sid = self.get_session_token() - cj = http_cookiejar.LWPCookieJar() - c = http_cookiejar.Cookie( + cj = http.cookiejar.LWPCookieJar() + c = http.cookiejar.Cookie( "0", "_session_id", sid, @@ -4663,13 +4664,13 @@ def _send_form(self, url, params): raise ShotgunError("Max attemps limit reached.") -class CACertsHTTPSConnection(http_client.HTTPConnection): +class CACertsHTTPSConnection(http.client.HTTPConnection): """ " This class allows to create an HTTPS connection that uses the custom certificates passed in. """ - default_port = http_client.HTTPS_PORT + default_port = http.client.HTTPS_PORT def __init__(self, *args, **kwargs): """ diff --git a/tests/base.py b/tests/base.py index cc8634996..48df427a1 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,11 +1,13 @@ """Base class for Flow Production Tracking API tests.""" +import configparser import contextlib import os import random import re import time import unittest +import urllib.error from . import mock @@ -13,8 +15,6 @@ from shotgun_api3.shotgun import json from shotgun_api3.shotgun import ServerCapabilities from shotgun_api3.lib import six -from shotgun_api3.lib.six.moves import urllib -from shotgun_api3.lib.six.moves.configparser import ConfigParser try: # Attempt to import skip from unittest. Since this was added in Python 2.7 @@ -456,7 +456,7 @@ def config_keys(self): ] def read_config(self, config_path): - config_parser = ConfigParser() + config_parser = configparser.ConfigParser() config_parser.read(config_path) for section in config_parser.sections(): for option in config_parser.options(section): diff --git a/tests/test_api.py b/tests/test_api.py index 751ace799..36f3ef375 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -22,6 +22,9 @@ import time import types import unittest +import urllib.parse +import urllib.request +import urllib.error import uuid import warnings @@ -33,8 +36,6 @@ # class for the current Python version. from shotgun_api3.lib.sgsix import ShotgunSSLError -from shotgun_api3.lib.six.moves import range, urllib - import shotgun_api3 from . import base diff --git a/tests/test_client.py b/tests/test_client.py index 9cb50a168..98d2ab127 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -19,8 +19,9 @@ import sys import time import unittest +import urllib.parse +import urllib.error -from shotgun_api3.lib.six.moves import urllib from shotgun_api3.lib import six, sgutils try: diff --git a/tests/test_unit.py b/tests/test_unit.py index ff78253c2..d2853e5df 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -13,11 +13,12 @@ import os import unittest from unittest import mock +import urllib.request +import urllib.error from .mock import patch import shotgun_api3 as api from shotgun_api3.shotgun import _is_mimetypes_broken -from shotgun_api3.lib.six.moves import range, urllib from shotgun_api3.lib.httplib2 import Http, ssl_error_classes From f68f1ae6c1161848067f04c15b81001256ca9294 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 16 Jul 2025 10:53:24 +0200 Subject: [PATCH 12/17] Cleanup BytesIO import from six --- shotgun_api3/shotgun.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shotgun_api3/shotgun.py b/shotgun_api3/shotgun.py index 338413810..77f2d57b5 100644 --- a/shotgun_api3/shotgun.py +++ b/shotgun_api3/shotgun.py @@ -34,6 +34,7 @@ import json import http.client # Used for secure file upload import http.cookiejar # used for attachment upload +import io # used for attachment upload import logging import os import re @@ -55,7 +56,6 @@ from .lib import six from .lib import sgsix from .lib import sgutils -from .lib.six import BytesIO # used for attachment upload from .lib.httplib2 import Http, ProxyInfo, socks, ssl_error_classes from .lib.sgtimezone import SgTimezone @@ -4434,7 +4434,7 @@ def _multipart_upload_file_to_storage(self, path, upload_info): data_size = len(data) # keep data as a stream so that we don't need to worry how it was # encoded. - data = BytesIO(data) + data = io.BytesIO(data) bytes_read += data_size part_url = self._get_upload_part_link( upload_info, filename, part_number @@ -4762,7 +4762,7 @@ def encode(self, params, files, boundary=None, buffer=None): # We'll do this across both python 2/3 rather than add more branching. boundary = uuid.uuid4() if buffer is None: - buffer = BytesIO() + buffer = io.BytesIO() for key, value in params: if isinstance(key, bytes): key = key.decode("utf-8") From 0b956cce92b6b99befae39f748b4d2452b9369b7 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 16 Jul 2025 10:58:18 +0200 Subject: [PATCH 13/17] simple json --- tests/base.py | 2 +- tests/test_client.py | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/base.py b/tests/base.py index 48df427a1..2e742ad8b 100644 --- a/tests/base.py +++ b/tests/base.py @@ -2,6 +2,7 @@ import configparser import contextlib +import json import os import random import re @@ -12,7 +13,6 @@ from . import mock import shotgun_api3 as api -from shotgun_api3.shotgun import json from shotgun_api3.shotgun import ServerCapabilities from shotgun_api3.lib import six diff --git a/tests/test_client.py b/tests/test_client.py index 98d2ab127..84fb94cb8 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -13,6 +13,7 @@ need a live server to run against.""" import datetime +import json import os import platform import re @@ -24,14 +25,6 @@ from shotgun_api3.lib import six, sgutils -try: - import simplejson as json -except ImportError: - try: - import json as json - except ImportError: - import shotgun_api3.lib.simplejson as json - from . import mock import shotgun_api3.lib.httplib2 as httplib2 From b94da1a72686ae332543ae7d1acd9cb72a486efa Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 16 Jul 2025 10:57:01 +0200 Subject: [PATCH 14/17] Cleanup Py2-3 compat with ImportError --- tests/base.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/base.py b/tests/base.py index 2e742ad8b..e30ec01a4 100644 --- a/tests/base.py +++ b/tests/base.py @@ -16,17 +16,6 @@ from shotgun_api3.shotgun import ServerCapabilities from shotgun_api3.lib import six -try: - # Attempt to import skip from unittest. Since this was added in Python 2.7 - # in the case that we're running on Python 2.6 we'll need a decorator to - # provide some equivalent functionality. - from unittest import skip -except ImportError: - # On Python 2.6 we'll just have to ignore tests that are skipped -- we won't - # mark them as skipped, but we will not fail on them. - def skip(f): - return lambda self: None - THUMBNAIL_MAX_ATTEMPTS = 30 THUMBNAIL_RETRY_INTERVAL = 10 From 6e89b9826e529d8859bbdb472adba73c9a44a14a Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 16 Jul 2025 11:14:54 +0200 Subject: [PATCH 15/17] Simplify Base64 --- shotgun_api3/shotgun.py | 14 +++++--------- tests/test_client.py | 8 ++------ 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/shotgun_api3/shotgun.py b/shotgun_api3/shotgun.py index 77f2d57b5..c7ab8a50d 100644 --- a/shotgun_api3/shotgun.py +++ b/shotgun_api3/shotgun.py @@ -29,6 +29,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ +import base64 import copy import datetime import json @@ -59,11 +60,6 @@ from .lib.httplib2 import Http, ProxyInfo, socks, ssl_error_classes from .lib.sgtimezone import SgTimezone -if six.PY3: - from base64 import encodebytes as base64encode -else: - from base64 import encodestring as base64encode - LOG = logging.getLogger("shotgun_api3") """ @@ -709,13 +705,13 @@ def __init__( # and auth header # Do NOT self._split_url(self.base_url) here, as it contains the lower - # case version of the base_url argument. Doing so would base64encode + # case version of the base_url argument. Doing so would base64.encodebytes # the lowercase version of the credentials. auth, self.config.server = self._split_url(base_url) if auth: - auth = base64encode(urllib.parse.unquote(auth).encode("utf-8")).decode( - "utf-8" - ) + auth = base64.encodebytes( + urllib.parse.unquote(auth).encode("utf-8") + ).decode("utf-8") self.config.authorization = "Basic " + auth.strip() # foo:bar@123.456.789.012:3456 diff --git a/tests/test_client.py b/tests/test_client.py index 84fb94cb8..ae1f1345e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -12,6 +12,7 @@ CRUD functions. These tests always use a mock http connection so not not need a live server to run against.""" +import base64 import datetime import json import os @@ -32,17 +33,12 @@ from shotgun_api3.shotgun import ServerCapabilities, SG_TIMEZONE from . import base -if six.PY3: - from base64 import encodebytes as base64encode -else: - from base64 import encodestring as base64encode - def b64encode(val): if isinstance(val, str): val = val.encode("utf-8") - return base64encode(val).decode("utf-8") + return base64.encodebytes(val).decode("utf-8") class TestShotgunClient(base.MockTestBase): From 439d0b3db81b3da4c4f66e77a1367c1ca7ac1f8b Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 17 Jul 2025 16:16:02 +0200 Subject: [PATCH 16/17] fixup! six.moves imports --- tests/test_api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 36f3ef375..5c618d7a9 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -630,7 +630,7 @@ def test_linked_thumbnail_url(self): # For now skip tests that are erroneously failling on some sites to # allow CI to pass until the known issue causing this is resolved. - @base.skip("Skipping test that erroneously fails on some sites.") + @unittest.skip("Skipping test that erroneously fails on some sites.") def test_share_thumbnail(self): """share thumbnail between two entities""" @@ -2934,7 +2934,7 @@ def _check_attachment(self, data, attachment_id, additional_fields): # For now skip tests that are erroneously failling on some sites to # allow CI to pass until the known issue causing this is resolved. - @base.skip("Skipping test that erroneously fails on some sites.") + @unittest.skip("Skipping test that erroneously fails on some sites.") def test_simple(self): """ Test note reply thread API call @@ -3013,7 +3013,7 @@ def test_simple(self): # For now skip tests that are erroneously failling on some sites to # allow CI to pass until the known issue causing this is resolved. - @base.skip("Skipping test that erroneously fails on some sites.") + @unittest.skip("Skipping test that erroneously fails on some sites.") def test_complex(self): """ Test note reply thread API call with additional params From adbc0da846e85af856a3b9d9c5c81d17ccbfce79 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 17 Jul 2025 16:29:08 +0200 Subject: [PATCH 17/17] fixup! six.moves imports --- tests/test_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_client.py b/tests/test_client.py index ae1f1345e..27e384b12 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -12,6 +12,7 @@ CRUD functions. These tests always use a mock http connection so not not need a live server to run against.""" +import configparser import base64 import datetime import json @@ -180,7 +181,7 @@ def test_read_config(self): """Validate that config values are properly coerced.""" this_dir = os.path.dirname(os.path.realpath(__file__)) config_path = os.path.join(this_dir, "test_config_file") - config = base.ConfigParser() + config = configparser.ConfigParser() config.read(config_path) result = config.get("SERVER_INFO", "api_key") expected = "%abce"