diff --git a/dropbox/base.py b/dropbox/base.py index 89a1e986..c21c1cea 100644 --- a/dropbox/base.py +++ b/dropbox/base.py @@ -92,7 +92,7 @@ def files_alpha_upload(self, larger than 150 MB. Instead, create an upload session with :meth:`files_upload_session_start`. - :param f: A string or file-like obj of data. + :param bytes f: Contents to upload. :param Nullable property_groups: List of custom properties to add to file. :rtype: :class:`dropbox.files.FileMetadata` @@ -1190,7 +1190,7 @@ def files_upload(self, this to upload a file larger than 150 MB. Instead, create an upload session with :meth:`files_upload_session_start`. - :param f: A string or file-like obj of data. + :param bytes f: Contents to upload. :param str path: Path in the user's Dropbox to save the file. :param mode: Selects what to do if the file already exists. :type mode: :class:`dropbox.files.WriteMode` @@ -1234,7 +1234,7 @@ def files_upload_session_append(self, Append more data to an upload session. A single request should not upload more than 150 MB of file contents. - :param f: A string or file-like obj of data. + :param bytes f: Contents to upload. :param str session_id: The upload session ID (returned by :meth:`files_upload_session_start`). :param long offset: The amount of data that has been uploaded so far. We @@ -1269,7 +1269,7 @@ def files_upload_session_append_v2(self, this call will close the session. A single request should not upload more than 150 MB of file contents. - :param f: A string or file-like obj of data. + :param bytes f: Contents to upload. :param cursor: Contains the upload session ID and the offset. :type cursor: :class:`dropbox.files.UploadSessionCursor` :param bool close: If true, the current session will be closed, at which @@ -1301,7 +1301,7 @@ def files_upload_session_finish(self, path. A single request should not upload more than 150 MB of file contents. - :param f: A string or file-like obj of data. + :param bytes f: Contents to upload. :param cursor: Contains the upload session ID and the offset. :type cursor: :class:`dropbox.files.UploadSessionCursor` :param commit: Contains the path and other optional modifiers for the @@ -1390,7 +1390,7 @@ def files_upload_session_start(self, Dropbox. A single request should not upload more than 150 MB of file contents. - :param f: A string or file-like obj of data. + :param bytes f: Contents to upload. :param bool close: If true, the current session will be closed, at which point you won't be able to call :meth:`files_upload_session_append_v2` anymore with the current diff --git a/dropbox/dropbox.py b/dropbox/dropbox.py index 90aaca0e..88df676f 100644 --- a/dropbox/dropbox.py +++ b/dropbox/dropbox.py @@ -291,7 +291,7 @@ def request_json_object(self, :param route_style: The style of the route. :param str request_arg: A JSON-serializable Python object representing the argument for the route. - :param request_binary: String or file pointer representing the binary + :param Optional[bytes] request_binary: Bytes representing the binary payload. Use None if there is no binary payload. :param Optional[float] timeout: Maximum duration in seconds that client will wait for any single packet from the @@ -376,6 +376,14 @@ def request_json_string(self, if host not in self._host_map: raise ValueError('Unknown value for host: %r' % host) + if not isinstance(request_binary, (six.binary_type, type(None))): + # Disallow streams and file-like objects even though the underlying + # requests library supports them. This is to prevent incorrect + # behavior when a non-rewindable stream is read from, but the + # request fails and needs to be re-tried at a later time. + raise TypeError('expected request_binary as binary type, got %s' % + type(request_binary)) + # Fully qualified hostname fq_hostname = self._host_map[host] url = self._get_route_url(fq_hostname, func_name) diff --git a/dropbox/stone_base.py b/dropbox/stone_base.py index cdefd988..4c7af725 100644 --- a/dropbox/stone_base.py +++ b/dropbox/stone_base.py @@ -34,6 +34,20 @@ def __init__(self, tag, value=None): self._tag = tag self._value = value + def __eq__(self, other): + # Also need to check if one class is a subclass of another. If one union extends another, + # the common fields should be able to be compared to each other. + return ( + isinstance(other, Union) and + (isinstance(self, other.__class__) or isinstance(other, self.__class__)) and + self._tag == other._tag and self._value == other._value + ) + + def __ne__(self, other): + return not self == other + + def __hash__(self): + return hash((self._tag, self._value)) class Route(object): diff --git a/stone b/stone index a69b8889..bc3e5e66 160000 --- a/stone +++ b/stone @@ -1 +1 @@ -Subproject commit a69b888936a8a69037cadb72ebb6679efc8d0d8f +Subproject commit bc3e5e668799e20c48c6495271e3ab9bc76353ff diff --git a/test/test_dropbox.py b/test/test_dropbox.py index 7a323e0a..3bd0695a 100644 --- a/test/test_dropbox.py +++ b/test/test_dropbox.py @@ -7,6 +7,11 @@ import sys import unittest +try: + from StringIO import StringIO as BytesIO +except ImportError: + from io import BytesIO + from dropbox import ( Dropbox, DropboxTeam, @@ -45,6 +50,8 @@ def inner(*args, **kwargs): MALFORMED_TOKEN = 'asdf' INVALID_TOKEN = 'z' * 62 +# Need bytes type for Python3 +DUMMY_PAYLOAD = string.ascii_letters.encode('ascii') class TestDropbox(unittest.TestCase): @@ -80,16 +87,20 @@ def test_upload_download(self): timestamp = str(datetime.datetime.utcnow()) random_filename = ''.join(random.sample(string.ascii_letters, 15)) random_path = '/Test/%s/%s' % (timestamp, random_filename) - test_contents = string.ascii_letters + test_contents = DUMMY_PAYLOAD self.dbx.files_upload(test_contents, random_path) # Download file metadata, resp = self.dbx.files_download(random_path) - self.assertEqual(string.ascii_letters, resp.text) + self.assertEqual(DUMMY_PAYLOAD, resp.content) # Cleanup folder self.dbx.files_delete('/Test/%s' % timestamp) + def test_bad_upload_types(self): + with self.assertRaises(TypeError): + self.dbx.files_upload(BytesIO(b'test'), '/Test') + @require_team_token def test_team(self, token): dbxt = DropboxTeam(token)