Skip to content

Commit 26178fc

Browse files
committed
Fix non-resumable binary uploads on Python 3
1. Generator and StringIO are replaced by BytesGenerator and BytesIO. If BytesGenerator doesn't exist (as is the case in Python 2), fall back to Generator. 2. BytesGenerator is buggy [1] [2] and corrupts '\r' into '\n'. To work around this, we implement a patched version of BytesGenerator that replaces ._write_lines with just .write. The test_multipart_media_good_upload has been updated to reflect the change. It is also stricter now, as it matches the entire request body against the expected form. Note: BytesGenerator was introduced in Python 3.2. This is OK since the library already demands 3.3+. Fixes #145. [1]: https://bugs.python.org/issue18886 [2]: https://bugs.python.org/issue19003
1 parent 7f85278 commit 26178fc

File tree

2 files changed

+27
-5
lines changed

2 files changed

+27
-5
lines changed

googleapiclient/discovery.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,17 @@
2828
'key2param',
2929
]
3030

31-
from six import StringIO
31+
from six import BytesIO
3232
from six.moves import http_client
3333
from six.moves.urllib.parse import urlencode, urlparse, urljoin, \
3434
urlunparse, parse_qsl
3535

3636
# Standard library imports
3737
import copy
38-
from email.generator import Generator
38+
try:
39+
from email.generator import BytesGenerator
40+
except ImportError:
41+
from email.generator import Generator as BytesGenerator
3942
from email.mime.multipart import MIMEMultipart
4043
from email.mime.nonmultipart import MIMENonMultipart
4144
import json
@@ -102,6 +105,10 @@
102105
# Library-specific reserved words beyond Python keywords.
103106
RESERVED_WORDS = frozenset(['body'])
104107

108+
# patch _write_lines to avoid munging '\r' into '\n'
109+
# ( https://bugs.python.org/issue18886 https://bugs.python.org/issue19003 )
110+
class _BytesGenerator(BytesGenerator):
111+
_write_lines = BytesGenerator.write
105112

106113
def fix_method_name(name):
107114
"""Fix method names to avoid reserved word conflicts.
@@ -797,8 +804,8 @@ def method(self, **kwargs):
797804
msgRoot.attach(msg)
798805
# encode the body: note that we can't use `as_string`, because
799806
# it plays games with `From ` lines.
800-
fp = StringIO()
801-
g = Generator(fp, mangle_from_=False)
807+
fp = BytesIO()
808+
g = _BytesGenerator(fp, mangle_from_=False)
802809
g.flatten(msgRoot, unixfrom=False)
803810
body = fp.getvalue()
804811

tests/test_discovery.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import json
3636
import os
3737
import pickle
38+
import re
3839
import sys
3940
import unittest2 as unittest
4041

@@ -787,7 +788,21 @@ def test_multipart_media_good_upload(self):
787788
request = zoo.animals().insert(media_body=datafile('small.png'), body={})
788789
self.assertTrue(request.headers['content-type'].startswith(
789790
'multipart/related'))
790-
self.assertEquals('--==', request.body[0:4])
791+
with open(datafile('small.png'), 'rb') as f:
792+
contents = f.read()
793+
boundary = re.match(b'--=+([^=]+)', request.body).group(1)
794+
self.assertEqual(
795+
request.body.rstrip(b"\n"), # Python 2.6 does not add a trailing \n
796+
b'--===============' + boundary + b'==\n' +
797+
b'Content-Type: application/json\n' +
798+
b'MIME-Version: 1.0\n\n' +
799+
b'{"data": {}}\n' +
800+
b'--===============' + boundary + b'==\n' +
801+
b'Content-Type: image/png\n' +
802+
b'MIME-Version: 1.0\n' +
803+
b'Content-Transfer-Encoding: binary\n\n' +
804+
contents +
805+
b'\n--===============' + boundary + b'==--')
791806
assertUrisEqual(self,
792807
'https://www.googleapis.com/upload/zoo/v1/animals?uploadType=multipart&alt=json',
793808
request.uri)

0 commit comments

Comments
 (0)