From 9777db084671aee6b64205d4dec3eee10deee7ea Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 16 Jul 2025 10:02:34 +0200 Subject: [PATCH 1/9] 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 ef7f3bc8..83cf1ef7 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 0bf509b3..9425012b 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 2/9] 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 2820d495..cc863499 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 83cf1ef7..ccc9e544 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 b6e5e39b..9b3e6ba1 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 e7e4295e..ad478304 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 cb713cd9..7bf0d700 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 3/9] 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 626fedce..a192bea5 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 9425012b..29a34e99 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 4/9] 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 ccc9e544..751ace79 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 5/9] 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 e2e050e8..96c91746 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 a192bea5..7b8bee79 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 9b3e6ba1..0c62fb9c 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 6/9] 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 0c62fb9c..11b33acc 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 7/9] 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 11b33acc..9cb50a16 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 8/9] 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 7b8bee79..6aaaeead 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 9/9] 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 6aaaeead..2bede5da 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