diff options
author | Daniel G. Taylor <danielgtaylor@gmail.com> | 2014-08-04 15:42:30 -0700 |
---|---|---|
committer | Daniel G. Taylor <danielgtaylor@gmail.com> | 2014-08-04 15:42:30 -0700 |
commit | ff3d8159af3c816303785e023a4182aacb6aabf5 (patch) | |
tree | 3f9ccb173904eecef829d07f9acbddc6e4b444e5 | |
parent | 60603b6514268a76d9f84a8f381fd645271492a8 (diff) | |
parent | 8aea0d3381d22d7dc0a58c488db6f5d429083c9c (diff) | |
download | boto-ff3d8159af3c816303785e023a4182aacb6aabf5.tar.gz |
Merge branch 'release-2.32.1'2.32.1
Conflicts:
docs/source/index.rst
57 files changed, 524 insertions, 254 deletions
@@ -1,9 +1,9 @@ #### boto #### -boto 2.32.0 +boto 2.32.1 -Released: 30-Jul-2014 +Released: 04-Aug-2014 .. image:: https://travis-ci.org/boto/boto.png?branch=develop :target: https://travis-ci.org/boto/boto diff --git a/bin/fetch_file b/bin/fetch_file index 9315aec7..defb8b0e 100755 --- a/bin/fetch_file +++ b/bin/fetch_file @@ -20,6 +20,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # +import sys + + if __name__ == "__main__": from optparse import OptionParser usage = """%prog [options] URI @@ -34,10 +37,10 @@ The URI can be either an HTTP URL, or "s3://bucket_name/key_name" (options, args) = parser.parse_args() if len(args) < 1: parser.print_help() - exit(1) + sys.exit(1) from boto.utils import fetch_file f = fetch_file(args[0]) if options.outfile: open(options.outfile, "w").write(f.read()) else: - print f.read() + print(f.read()) diff --git a/bin/glacier b/bin/glacier index a3763e06..ec0364a0 100755 --- a/bin/glacier +++ b/bin/glacier @@ -41,7 +41,7 @@ COMMANDS = ('vaults', 'jobs', 'upload') def usage(): - print """ + print(""" glacier <command> [args] Commands @@ -78,7 +78,7 @@ glacier <command> [args] Examples : glacier upload pics *.jpg glacier upload pics a.jpg b.jpg -""" +""") sys.exit() @@ -89,7 +89,7 @@ def connect(region, debug_level=0, access_key=None, secret_key=None): aws_secret_access_key=secret_key, debug=debug_level) if layer2 is None: - print 'Invalid region (%s)' % region + print('Invalid region (%s)' % region) sys.exit(1) return layer2 @@ -97,12 +97,12 @@ def connect(region, debug_level=0, access_key=None, secret_key=None): def list_vaults(region, access_key=None, secret_key=None): layer2 = connect(region, access_key = access_key, secret_key = secret_key) for vault in layer2.list_vaults(): - print vault.arn + print(vault.arn) def list_jobs(vault_name, region, access_key=None, secret_key=None): layer2 = connect(region, access_key = access_key, secret_key = secret_key) - print layer2.layer1.list_jobs(vault_name) + print(layer2.layer1.list_jobs(vault_name)) def upload_files(vault_name, filenames, region, access_key=None, secret_key=None): @@ -111,7 +111,7 @@ def upload_files(vault_name, filenames, region, access_key=None, secret_key=None glacier_vault = layer2.get_vault(vault_name) for filename in filenames: if isfile(filename): - print 'Uploading %s to %s' % (filename, vault_name) + print('Uploading %s to %s' % (filename, vault_name)) glacier_vault.upload_archive(filename) @@ -128,7 +128,7 @@ def main(): long_options = ['access_key=', 'secret_key=', 'region='] try: opts, args = getopt(argv, options, long_options) - except GetoptError, e: + except GetoptError as e: usage() # Parse agument diff --git a/bin/kill_instance b/bin/kill_instance index 0c637413..6683bb83 100755 --- a/bin/kill_instance +++ b/bin/kill_instance @@ -13,7 +13,7 @@ def kill_instance(region, ids): # Connect the region ec2 = boto.connect_ec2(region=region) for instance_id in ids: - print "Stopping instance: %s" % instance_id + print("Stopping instance: %s" % instance_id) ec2.terminate_instances([instance_id]) @@ -29,7 +29,7 @@ if __name__ == "__main__": region = r break else: - print "Region %s not found." % options.region + print("Region %s not found." % options.region) sys.exit(1) kill_instance(region, args) diff --git a/bin/launch_instance b/bin/launch_instance index 77a5419c..62bc5e4d 100755 --- a/bin/launch_instance +++ b/bin/launch_instance @@ -33,7 +33,7 @@ f.close() import boto.pyami.config import boto.utils import re, os -import ConfigParser +from boto.compat import ConfigParser class Config(boto.pyami.config.Config): """A special config class that also adds import abilities @@ -43,7 +43,7 @@ class Config(boto.pyami.config.Config): """ def __init__(self): - ConfigParser.SafeConfigParser.__init__(self, {'working_dir' : '/mnt/pyami', 'debug' : '0'}) + ConfigParser.__init__(self, {'working_dir' : '/mnt/pyami', 'debug' : '0'}) def add_config(self, file_url): """Add a config file to this configuration @@ -150,7 +150,7 @@ if __name__ == "__main__": region = r break else: - print "Region %s not found." % options.region + print("Region %s not found." % options.region) sys.exit(1) ec2 = boto.connect_ec2(region=region) if not options.nocred and not options.role: @@ -206,7 +206,7 @@ if __name__ == "__main__": scr_url = "file://%s" % scr_url try: scriptuples.append((scr, add_script(scr_url))) - except Exception, e: + except Exception as e: pass user_data = boto.utils.write_mime_multipart(scriptuples, compress=True) @@ -248,5 +248,5 @@ if __name__ == "__main__": time.sleep(3) if options.dns: - print "Public DNS name: %s" % instance.public_dns_name - print "Private DNS name: %s" % instance.private_dns_name + print("Public DNS name: %s" % instance.public_dns_name) + print("Private DNS name: %s" % instance.private_dns_name) diff --git a/bin/list_instances b/bin/list_instances index 8cb743c0..e48ec390 100755 --- a/bin/list_instances +++ b/bin/list_instances @@ -45,7 +45,7 @@ def main(): region = r break else: - print "Region %s not found." % options.region + print("Region %s not found." % options.region) sys.exit(1) ec2 = boto.connect_ec2(region=region) @@ -73,17 +73,17 @@ def main(): # List and print if not options.tab: - print format_string % headers - print "-" * len(format_string % headers) + print(format_string % headers) + print("-" * len(format_string % headers)) for r in ec2.get_all_reservations(filters=filters): groups = [g.name for g in r.groups] for i in r.instances: i.groups = ','.join(groups) if options.tab: - print "\t".join(tuple(get_column(h, i) for h in headers)) + print("\t".join(tuple(get_column(h, i) for h in headers))) else: - print format_string % tuple(get_column(h, i) for h in headers) + print(format_string % tuple(get_column(h, i) for h in headers)) if __name__ == "__main__": @@ -22,7 +22,7 @@ def list_bucket(b, prefix=None, marker=None): if not prefix.endswith("/"): prefix = prefix + "/" query = b.list(prefix=prefix, delimiter="/", marker=marker) - print "%s" % prefix + print("%s" % prefix) else: query = b.list(delimiter="/", marker=marker) @@ -42,27 +42,27 @@ def list_bucket(b, prefix=None, marker=None): elif g.permission == "FULL_CONTROL": mode = "-rwxrwx" if isinstance(k, Key): - print "%s\t%s\t%010s\t%s" % (mode, k.last_modified, - sizeof_fmt(size), k.name) + print("%s\t%s\t%010s\t%s" % (mode, k.last_modified, + sizeof_fmt(size), k.name)) else: #If it's not a Key object, it doesn't have a last_modified time, so #print nothing instead - print "%s\t%s\t%010s\t%s" % (mode, ' ' * 24, - sizeof_fmt(size), k.name) + print("%s\t%s\t%010s\t%s" % (mode, ' ' * 24, + sizeof_fmt(size), k.name)) total += size - print "=" * 80 - print "\t\tTOTAL: \t%010s \t%i Files" % (sizeof_fmt(total), num) + print ("=" * 80) + print ("\t\tTOTAL: \t%010s \t%i Files" % (sizeof_fmt(total), num)) def list_buckets(s3, display_tags=False): """List all the buckets""" for b in s3.get_all_buckets(): - print b.name + print(b.name) if display_tags: try: tags = b.get_tags() for tag in tags[0]: - print " %s:%s" % (tag.key, tag.value) + print(" %s:%s" % (tag.key, tag.value)) except S3ResponseError as e: if e.status != 404: raise @@ -87,7 +87,7 @@ def main(): sys.exit(0) if options.tags: - print "-t option only works for the overall bucket list" + print("-t option only works for the overall bucket list") sys.exit(1) pairs = [] @@ -25,6 +25,8 @@ import sys import os import boto +from boto.compat import six + try: # multipart portions copyright Fabian Topfstedt # https://gist.github.com/924094 @@ -43,8 +45,12 @@ try: except ImportError as err: multipart_capable = False usage_flag_multipart_capable = "" + if six.PY2: + attribute = 'message' + else: + attribute = 'msg' usage_string_multipart_capable = '\n\n "' + \ - err.message[len('No module named '):] + \ + getattr(err, attribute)[len('No module named '):] + \ '" is missing for multipart support ' @@ -121,12 +127,12 @@ SYNOPSIS def usage(status=1): - print usage_string + print(usage_string) sys.exit(status) def submit_cb(bytes_so_far, total_bytes): - print '%d bytes transferred / %d bytes total' % (bytes_so_far, total_bytes) + print('%d bytes transferred / %d bytes total' % (bytes_so_far, total_bytes)) def get_key_name(fullpath, prefix, key_prefix): @@ -145,12 +151,12 @@ def _upload_part(bucketname, aws_key, aws_secret, multipart_id, part_num, Uploads a part with retries. """ if debug == 1: - print "_upload_part(%s, %s, %s)" % (source_path, offset, bytes) + print("_upload_part(%s, %s, %s)" % (source_path, offset, bytes)) def _upload(retries_left=amount_of_retries): try: if debug == 1: - print 'Start uploading part #%d ...' % part_num + print('Start uploading part #%d ...' % part_num) conn = S3Connection(aws_key, aws_secret) conn.debug = debug bucket = conn.get_bucket(bucketname) @@ -161,21 +167,21 @@ def _upload_part(bucketname, aws_key, aws_secret, multipart_id, part_num, mp.upload_part_from_file(fp=fp, part_num=part_num, cb=cb, num_cb=num_cb) break - except Exception, exc: + except Exception as exc: if retries_left: _upload(retries_left=retries_left - 1) else: - print 'Failed uploading part #%d' % part_num + print('Failed uploading part #%d' % part_num) raise exc else: if debug == 1: - print '... Uploaded part #%d' % part_num + print('... Uploaded part #%d' % part_num) _upload() def check_valid_region(conn, region): if conn is None: - print 'Invalid region (%s)' % region + print('Invalid region (%s)' % region) sys.exit(1) def multipart_upload(bucketname, aws_key, aws_secret, source_path, keyname, @@ -312,7 +318,7 @@ def main(): if multipart_capable: multipart_requested = True else: - print "multipart upload requested but not capable" + print("multipart upload requested but not capable") sys.exit(4) if o == '--region': regions = boto.s3.regions() @@ -327,7 +333,7 @@ def main(): usage(2) if not bucket_name: - print "bucket name is required!" + print("bucket name is required!") usage(3) connect_args = { @@ -352,23 +358,23 @@ def main(): # Classic region will be '', any other will have a name if location: - print 'Bucket exists in %s but no host or region given!' % location + print('Bucket exists in %s but no host or region given!' % location) # Override for EU, which is really Ireland according to the docs if location == 'EU': location = 'eu-west-1' - print 'Automatically setting region to %s' % location + print('Automatically setting region to %s' % location) # Here we create a new connection, and then take the existing # bucket and set it to use the new connection c = boto.s3.connect_to_region(location, **connect_args) c.debug = debug b.connection = c - except Exception, e: + except Exception as e: if debug > 0: - print e - print 'Could not get bucket region info, skipping...' + print(e) + print('Could not get bucket region info, skipping...') existing_keys_to_check_against = [] files_to_check_for_upload = [] @@ -379,7 +385,7 @@ def main(): if os.path.isdir(path): if no_overwrite: if not quiet: - print 'Getting list of existing keys to check against' + print('Getting list of existing keys to check against') for key in b.list(get_key_name(path, prefix, key_prefix)): existing_keys_to_check_against.append(key.name) for root, dirs, files in os.walk(path): @@ -400,7 +406,7 @@ def main(): # we are trying to upload something unknown else: - print "I don't know what %s is, so i can't upload it" % path + print("I don't know what %s is, so i can't upload it" % path) for fullpath in files_to_check_for_upload: key_name = get_key_name(fullpath, prefix, key_prefix) @@ -408,11 +414,11 @@ def main(): if no_overwrite and key_name in existing_keys_to_check_against: if b.get_key(key_name): if not quiet: - print 'Skipping %s as it exists in s3' % fullpath + print('Skipping %s as it exists in s3' % fullpath) continue if not quiet: - print 'Copying %s to %s/%s' % (fullpath, bucket_name, key_name) + print('Copying %s to %s/%s' % (fullpath, bucket_name, key_name)) if not no_op: # 0-byte files don't work and also don't need multipart upload diff --git a/boto/__init__.py b/boto/__init__.py index 5b142e97..3d90e901 100644 --- a/boto/__init__.py +++ b/boto/__init__.py @@ -38,7 +38,7 @@ import logging.config from boto.compat import urlparse from boto.exception import InvalidUriError -__version__ = '2.32.0' +__version__ = '2.32.1' Version = __version__ # for backware compatibility # http://bugs.python.org/issue7980 diff --git a/boto/auth.py b/boto/auth.py index 6d431461..6012962a 100644 --- a/boto/auth.py +++ b/boto/auth.py @@ -41,7 +41,7 @@ import sys import time import posixpath -from boto.compat import urllib +from boto.compat import urllib, encodebytes from boto.auth_handler import AuthHandler from boto.exception import BotoClientError @@ -89,7 +89,7 @@ class HmacKeys(object): def sign_string(self, string_to_sign): new_hmac = self._get_hmac() new_hmac.update(string_to_sign.encode('utf-8')) - return base64.encodestring(new_hmac.digest()).decode('utf-8').strip() + return encodebytes(new_hmac.digest()).decode('utf-8').strip() def __getstate__(self): pickled_dict = copy.copy(self.__dict__) @@ -99,7 +99,7 @@ class HmacKeys(object): def __setstate__(self, dct): self.__dict__ = dct - self.update_provider(self._provider.encode('utf-8')) + self.update_provider(self._provider) class AnonAuthHandler(AuthHandler, HmacKeys): diff --git a/boto/cloudsearch/document.py b/boto/cloudsearch/document.py index b5a6b455..0a1d9db2 100644 --- a/boto/cloudsearch/document.py +++ b/boto/cloudsearch/document.py @@ -240,6 +240,9 @@ class CommitResponse(object): raise EncodingError("Illegal Unicode character in document") elif e == "The Content-Length is too long": raise ContentTooLongError("Content was too long") + if 'adds' not in self.content or 'deletes' not in self.content: + raise SearchServiceException("Error indexing documents" + " => %s" % self.content.get('message', '')) else: self.errors = [] diff --git a/boto/compat.py b/boto/compat.py index 21a9193d..a7503f01 100644 --- a/boto/compat.py +++ b/boto/compat.py @@ -29,6 +29,13 @@ except ImportError: import json +# Switch to use encodebytes, which deprecates encodestring in Python 3 +try: + from base64 import encodebytes +except ImportError: + from base64 import encodestring as encodebytes + + # If running in Google App Engine there is no "user" and # os.path.expanduser() will fail. Attempt to detect this case and use a # no-op expanduser function in this case. @@ -45,7 +52,6 @@ from boto.vendored.six import BytesIO, StringIO from boto.vendored.six.moves import filter, http_client, map, _thread, \ urllib, zip from boto.vendored.six.moves.queue import Queue -from boto.vendored.six.moves.configparser import SafeConfigParser from boto.vendored.six.moves.urllib.parse import parse_qs, quote, unquote, \ urlparse, urlsplit from boto.vendored.six.moves.urllib.request import urlopen @@ -54,6 +60,8 @@ if six.PY3: # StandardError was removed, so use the base exception type instead StandardError = Exception long_type = int + from configparser import ConfigParser else: StandardError = StandardError long_type = long + from ConfigParser import SafeConfigParser as ConfigParser diff --git a/boto/connection.py b/boto/connection.py index 9846a879..5fe9c198 100644 --- a/boto/connection.py +++ b/boto/connection.py @@ -42,9 +42,6 @@ """ Handles basic connections to AWS """ -from __future__ import with_statement - -import base64 from datetime import datetime import errno import os @@ -64,7 +61,7 @@ import boto.handler import boto.cacerts from boto import config, UserAgent -from boto.compat import six, http_client, urlparse, quote +from boto.compat import six, http_client, urlparse, quote, encodebytes from boto.exception import AWSConnectionError from boto.exception import BotoClientError from boto.exception import BotoServerError @@ -857,7 +854,7 @@ class AWSAuthConnection(object): return path def get_proxy_auth_header(self): - auth = base64.encodestring(self.proxy_user + ':' + self.proxy_pass) + auth = encodebytes(self.proxy_user + ':' + self.proxy_pass) return {'Proxy-Authorization': 'Basic %s' % auth} def set_host_header(self, request): diff --git a/boto/dynamodb/types.py b/boto/dynamodb/types.py index 5e3bae69..2049c219 100644 --- a/boto/dynamodb/types.py +++ b/boto/dynamodb/types.py @@ -148,10 +148,10 @@ def dynamize_value(val): if six.PY2: class Binary(object): def __init__(self, value): - if isinstance(value, six.text_type): # Support only PY2 for backward compatibility. - value = value.encode('utf-8') - elif not isinstance(value, bytes): + if not isinstance(value, (bytes, six.text_type)): raise TypeError('Value must be a string of binary data!') + if not isinstance(value, bytes): + value = value.encode("utf-8") self.value = value @@ -171,7 +171,7 @@ if six.PY2: return 'Binary(%r)' % self.value def __str__(self): - return self.value.decode('utf-8') + return self.value def __hash__(self): return hash(self.value) diff --git a/boto/emr/emrobject.py b/boto/emr/emrobject.py index 0906bfab..f605834c 100644 --- a/boto/emr/emrobject.py +++ b/boto/emr/emrobject.py @@ -301,7 +301,7 @@ class ClusterSummaryList(EmrObject): class StepConfig(EmrObject): Fields = set([ - 'Jar' + 'Jar', 'MainClass' ]) @@ -434,11 +434,15 @@ class StepSummary(EmrObject): def __init__(self, connection=None): self.connection = connection self.status = None + self.config = None def startElement(self, name, attrs, connection): if name == 'Status': self.status = ClusterStatus() return self.status + elif name == 'Config': + self.config = StepConfig() + return self.config else: return None diff --git a/boto/fps/connection.py b/boto/fps/connection.py index e8124ab2..6dc90a24 100644 --- a/boto/fps/connection.py +++ b/boto/fps/connection.py @@ -23,7 +23,6 @@ import urllib import uuid -from boto.compat import filter, map from boto.connection import AWSQueryConnection from boto.fps.exception import ResponseErrorFactory from boto.fps.response import ResponseFactory diff --git a/boto/glacier/concurrent.py b/boto/glacier/concurrent.py index 93a12d4f..a4f3a224 100644 --- a/boto/glacier/concurrent.py +++ b/boto/glacier/concurrent.py @@ -19,8 +19,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # -from __future__ import with_statement - import os import math import threading diff --git a/boto/glacier/job.py b/boto/glacier/job.py index 097312a1..33e66a19 100644 --- a/boto/glacier/job.py +++ b/boto/glacier/job.py @@ -20,7 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # -from __future__ import with_statement import math import socket @@ -124,7 +123,7 @@ class Job(object): verify_hashes, retry_exceptions) def download_to_fileobj(self, output_file, chunk_size=DefaultPartSize, - verify_hashes=True, + verify_hashes=True, retry_exceptions=(socket.error,)): """Download an archive to a file object. diff --git a/boto/glacier/layer1.py b/boto/glacier/layer1.py index 45a142d8..39136cf0 100644 --- a/boto/glacier/layer1.py +++ b/boto/glacier/layer1.py @@ -89,12 +89,13 @@ class Layer1(AWSAuthConnection): self.region = region self.account_id = account_id super(Layer1, self).__init__(region.endpoint, - aws_access_key_id, aws_secret_access_key, - is_secure, port, proxy, proxy_port, - proxy_user, proxy_pass, debug, - https_connection_factory, - path, provider, security_token, - suppress_consec_slashes, profile_name=profile_name) + aws_access_key_id, aws_secret_access_key, + is_secure, port, proxy, proxy_port, + proxy_user, proxy_pass, debug, + https_connection_factory, + path, provider, security_token, + suppress_consec_slashes, + profile_name=profile_name) def _required_auth_capability(self): return ['hmac-v4'] @@ -107,10 +108,10 @@ class Layer1(AWSAuthConnection): headers['x-amz-glacier-version'] = self.Version uri = '/%s/%s' % (self.account_id, resource) response = super(Layer1, self).make_request(verb, uri, - params=params, - headers=headers, - sender=sender, - data=data) + params=params, + headers=headers, + sender=sender, + data=data) if response.status in ok_responses: return GlacierResponse(response, response_headers) else: @@ -826,9 +827,9 @@ class Layer1(AWSAuthConnection): else: sender = None return self.make_request('POST', uri, headers=headers, - sender=sender, - data=archive, ok_responses=(201,), - response_headers=response_headers) + sender=sender, + data=archive, ok_responses=(201,), + response_headers=response_headers) def _is_file_like(self, archive): return hasattr(archive, 'seek') and hasattr(archive, 'tell') diff --git a/boto/glacier/utils.py b/boto/glacier/utils.py index 21da67ba..98847e3f 100644 --- a/boto/glacier/utils.py +++ b/boto/glacier/utils.py @@ -23,6 +23,8 @@ import hashlib import math import binascii +from boto.compat import six + _MEGABYTE = 1024 * 1024 DEFAULT_PART_SIZE = 4 * _MEGABYTE @@ -122,10 +124,19 @@ def compute_hashes_from_fileobj(fileobj, chunk_size=1024 * 1024): are returned in hex. """ + # Python 3+, not binary + if six.PY3 and hasattr(fileobj, 'mode') and 'b' not in fileobj.mode: + raise ValueError('File-like object must be opened in binary mode!') + linear_hash = hashlib.sha256() chunks = [] - chunk = fileobj.read(chunk_size).encode('utf-8') + chunk = fileobj.read(chunk_size) while chunk: + # It's possible to get a file-like object that has no mode (checked + # above) and returns something other than bytes (e.g. str). So here + # we try to catch that and encode to bytes. + if not isinstance(chunk, bytes): + chunk = chunk.encode(getattr(fileobj, 'encoding', '') or 'utf-8') linear_hash.update(chunk) chunks.append(hashlib.sha256(chunk).digest()) chunk = fileobj.read(chunk_size) diff --git a/boto/glacier/vault.py b/boto/glacier/vault.py index e447c188..45d276ca 100644 --- a/boto/glacier/vault.py +++ b/boto/glacier/vault.py @@ -21,9 +21,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # -from __future__ import with_statement import codecs -from boto.compat import six from boto.glacier.exceptions import UploadArchiveError from boto.glacier.job import Job from boto.glacier.writer import compute_hashes_from_fileobj, \ diff --git a/boto/gs/resumable_upload_handler.py b/boto/gs/resumable_upload_handler.py index 5b88b621..d7443469 100644 --- a/boto/gs/resumable_upload_handler.py +++ b/boto/gs/resumable_upload_handler.py @@ -26,16 +26,13 @@ import re import socket import time import urlparse +from hashlib import md5 from boto import config, UserAgent from boto.connection import AWSAuthConnection from boto.exception import InvalidUriError from boto.exception import ResumableTransferDisposition from boto.exception import ResumableUploadException from boto.s3.keyfile import KeyFile -try: - from hashlib import md5 -except ImportError: - from md5 import md5 """ Handler for Google Cloud Storage resumable uploads. See diff --git a/boto/manage/volume.py b/boto/manage/volume.py index a4fcb5f3..410414c7 100644 --- a/boto/manage/volume.py +++ b/boto/manage/volume.py @@ -18,7 +18,7 @@ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. -from __future__ import with_statement, print_function +from __future__ import print_function from boto.sdb.db.model import Model from boto.sdb.db.property import StringProperty, IntegerProperty, ListProperty, ReferenceProperty, CalculatedProperty diff --git a/boto/mws/connection.py b/boto/mws/connection.py index 6759d028..01b0b30b 100644 --- a/boto/mws/connection.py +++ b/boto/mws/connection.py @@ -20,7 +20,6 @@ # IN THE SOFTWARE. import xml.sax import hashlib -import base64 import string import collections from boto.connection import AWSQueryConnection @@ -28,7 +27,7 @@ from boto.exception import BotoServerError import boto.mws.exception import boto.mws.response from boto.handler import XmlHandler -from boto.compat import filter, map, six +from boto.compat import filter, map, six, encodebytes __all__ = ['MWSConnection'] @@ -55,7 +54,7 @@ api_version_path = { 'OffAmazonPayments': ('2013-01-01', 'SellerId', '/OffAmazonPayments/2013-01-01'), } -content_md5 = lambda c: base64.encodestring(hashlib.md5(c).digest()).strip() +content_md5 = lambda c: encodebytes(hashlib.md5(c).digest()).strip() decorated_attrs = ('action', 'response', 'section', 'quota', 'restore', 'version') api_call_map = {} diff --git a/boto/pyami/config.py b/boto/pyami/config.py index 3789113b..37445f85 100644 --- a/boto/pyami/config.py +++ b/boto/pyami/config.py @@ -26,7 +26,7 @@ import warnings import boto -from boto.compat import expanduser, SafeConfigParser, StringIO +from boto.compat import expanduser, ConfigParser, StringIO # By default we use two locations for the boto configurations, @@ -49,12 +49,12 @@ elif 'BOTO_PATH' in os.environ: BotoConfigLocations.append(expanduser(path)) -class Config(SafeConfigParser): +class Config(ConfigParser): def __init__(self, path=None, fp=None, do_load=True): # We don't use ``super`` here, because ``ConfigParser`` still uses # old-style classes. - SafeConfigParser.__init__(self, {'working_dir': '/mnt/pyami', + ConfigParser.__init__(self, {'working_dir': '/mnt/pyami', 'debug': '0'}) if do_load: if path: @@ -95,7 +95,7 @@ class Config(SafeConfigParser): Replace any previous value. If the path doesn't exist, create it. Also add the option the the in-memory config. """ - config = SafeConfigParser() + config = ConfigParser() config.read(path) if not config.has_section(section): config.add_section(section) @@ -139,21 +139,21 @@ class Config(SafeConfigParser): def get(self, section, name, default=None): try: - val = SafeConfigParser.get(self, section, name) + val = ConfigParser.get(self, section, name) except: val = default return val def getint(self, section, name, default=0): try: - val = SafeConfigParser.getint(self, section, name) + val = ConfigParser.getint(self, section, name) except: val = int(default) return val def getfloat(self, section, name, default=0.0): try: - val = SafeConfigParser.getfloat(self, section, name) + val = ConfigParser.getfloat(self, section, name) except: val = float(default) return val diff --git a/boto/pyami/installers/ubuntu/mysql.py b/boto/pyami/installers/ubuntu/mysql.py index 8dd643bf..5b0792ba 100644 --- a/boto/pyami/installers/ubuntu/mysql.py +++ b/boto/pyami/installers/ubuntu/mysql.py @@ -31,7 +31,7 @@ from boto.pyami.installers.ubuntu.installer import Installer import os import boto from boto.utils import ShellCommand -from ConfigParser import SafeConfigParser +from boto.compat import ConfigParser import time ConfigSection = """ @@ -89,7 +89,7 @@ class MySQL(Installer): self.start('mysql') else: #get the password ubuntu expects to use: - config_parser = SafeConfigParser() + config_parser = ConfigParser() config_parser.read('/etc/mysql/debian.cnf') password = config_parser.get('client', 'password') # start the mysql deamon, then mysql with the required grant statement piped into it: diff --git a/boto/regioninfo.py b/boto/regioninfo.py index eee20bbf..5862f16d 100644 --- a/boto/regioninfo.py +++ b/boto/regioninfo.py @@ -20,7 +20,6 @@ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. -from __future__ import with_statement import os import boto diff --git a/boto/s3/key.py b/boto/s3/key.py index ec9889b1..9674d356 100644 --- a/boto/s3/key.py +++ b/boto/s3/key.py @@ -20,8 +20,6 @@ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. -from __future__ import with_statement - import email.utils import errno import hashlib @@ -31,8 +29,9 @@ import re import base64 import binascii import math +from hashlib import md5 import boto.utils -from boto.compat import BytesIO, six, urllib +from boto.compat import BytesIO, six, urllib, encodebytes from boto.exception import BotoClientError from boto.exception import StorageDataError @@ -45,11 +44,6 @@ from boto.utils import compute_md5, compute_hash from boto.utils import find_matching_headers from boto.utils import merge_headers_by_name -try: - from hashlib import md5 -except ImportError: - from md5 import md5 - class Key(object): """ @@ -211,7 +205,7 @@ class Key(object): from just having a precalculated md5_hexdigest. """ digest = binascii.unhexlify(md5_hexdigest) - base64md5 = base64.encodestring(digest) + base64md5 = encodebytes(digest) if base64md5[-1] == '\n': base64md5 = base64md5[0:-1] return (md5_hexdigest, base64md5) diff --git a/boto/sdb/db/manager/xmlmanager.py b/boto/sdb/db/manager/xmlmanager.py index d19ad088..f457347a 100644 --- a/boto/sdb/db/manager/xmlmanager.py +++ b/boto/sdb/db/manager/xmlmanager.py @@ -22,7 +22,7 @@ import boto from boto.utils import find_class, Password from boto.sdb.db.key import Key from boto.sdb.db.model import Model -from boto.compat import six +from boto.compat import six, encodebytes from datetime import datetime from xml.dom.minidom import getDOMImplementation, parse, parseString, Node @@ -203,8 +203,7 @@ class XMLManager(object): self.enable_ssl = enable_ssl self.auth_header = None if self.db_user: - import base64 - base64string = base64.encodestring('%s:%s' % (self.db_user, self.db_passwd))[:-1] + base64string = encodebytes('%s:%s' % (self.db_user, self.db_passwd))[:-1] authheader = "Basic %s" % base64string self.auth_header = authheader diff --git a/boto/utils.py b/boto/utils.py index 1569209b..dd5f0956 100644 --- a/boto/utils.py +++ b/boto/utils.py @@ -57,25 +57,14 @@ import email.mime.text import email.utils import email.encoders import gzip -import base64 import threading import locale -from boto.compat import six, StringIO, urllib +from boto.compat import six, StringIO, urllib, encodebytes from contextlib import contextmanager -try: - from hashlib import md5 -except ImportError: - from md5 import md5 - - -try: - import hashlib - _hashfn = hashlib.sha512 -except ImportError: - import md5 - _hashfn = md5.md5 +from hashlib import md5, sha512 +_hashfn = sha512 from boto.compat import json @@ -224,6 +213,11 @@ def retry_url(url, retry_on_404=True, num_retries=10): req = urllib.request.Request(url) r = opener.open(req) result = r.read() + + if(not isinstance(result, six.string_types) and + hasattr(result, 'decode')): + result = result.decode('utf-8') + return result except urllib.error.HTTPError as e: code = e.getcode() @@ -1029,7 +1023,7 @@ def compute_hash(fp, buf_size=8192, size=None, hash_algorithm=md5): else: s = fp.read(buf_size) hex_digest = hash_obj.hexdigest() - base64_digest = base64.encodestring(hash_obj.digest()).decode('utf-8') + base64_digest = encodebytes(hash_obj.digest()).decode('utf-8') if base64_digest[-1] == '\n': base64_digest = base64_digest[0:-1] # data_size based on bytes read. diff --git a/boto/vpc/vpc_peering_connection.py b/boto/vpc/vpc_peering_connection.py index d7c11e8e..cdb9af8d 100644 --- a/boto/vpc/vpc_peering_connection.py +++ b/boto/vpc/vpc_peering_connection.py @@ -145,7 +145,7 @@ class VpcPeeringConnection(TaggedEC2Object): setattr(self, name, value) def delete(self): - return self.connection.delete_vpc(self.id) + return self.connection.delete_vpc_peering_connection(self.id) def _update(self, updated): self.__dict__.update(updated.__dict__) diff --git a/docs/source/index.rst b/docs/source/index.rst index 36be2d2e..dd4ac6f5 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -132,6 +132,7 @@ Release Notes .. toctree:: :titlesonly: + releasenotes/v2.32.1 releasenotes/v2.32.0 releasenotes/v2.31.1 releasenotes/v2.31.0 diff --git a/docs/source/releasenotes/v2.32.1.rst b/docs/source/releasenotes/v2.32.1.rst new file mode 100644 index 00000000..4b06d797 --- /dev/null +++ b/docs/source/releasenotes/v2.32.1.rst @@ -0,0 +1,32 @@ +boto v2.32.1 +============ + +:date: 2014/08/04 + +This release fixes an incorrect Amazon VPC peering connection call, and fixes +several minor issues related to Python 3 support including a regression when +pickling authentication information. + + +Fixes +----- +* Fix bin scripts for Python 3. (:issue:`2502`, :issue:`2490`, :sha:`cb78c52`) +* Fix parsing of EMR step summary response. (:issue:`2456`, :sha:`2ffb00a`) +* Update wheel to be universal for py2/py3. (:issue:`2478`, :sha:`e872d94`) +* Add pypy to tox config. (:issue:`2458`, :sha:`16c6fbe`) +* Fix Glacier file object hash calculation. (:issue:`2489`, :issue:`2488`, + :sha:`a9463c5`) +* PEP8 fixes for Glacier. (:issue:`2469`, :sha:`0575a54`) +* Use ConfigParser for Python 3 and SafeConfigParser for Python 2. + (:issue:`2498`, :issue:`2497`, :sha:`f580f73`) +* Remove redundant __future__ imports. (:issue:`2496`, :sha:`e59e199`) +* Fix dynamodb.types.Binary non-ASCII handling. (:issue:`2492`, :issue:`2491`, + :sha:`16284ea`) +* Add missing dependency to requirements.txt. (:issue:`2494`, :sha:`33db71a`) +* Fix TypeError when getting instance metadata under Python 3. (:issue:`2486`, + :issue:`2485`, :sha:`6ff525e`) +* Handle Cloudsearch indexing errors. (:issue:`2370`, :sha:`494a091`) +* Remove obsolete md5 import routine. (:issue:`2468`, :sha:`9808a77`) +* Use encodebytes instead of encodestring. (:issue:`2484`, :issue:`2483`, + :sha:`984c5ff`) +* Fix an auth class pickling bug. (:issue:`2479`, :sha:`07d6424`) diff --git a/requirements.txt b/requirements.txt index d4416cbc..22756525 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ httpretty>=0.7.0 paramiko>=1.10.0 PyYAML>=3.10 coverage==3.7.1 +mock==1.0.1 @@ -1,2 +1,2 @@ [bdist_wheel] -python-tag = py2 +universal = 1 @@ -23,7 +23,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. -from __future__ import with_statement, print_function +from __future__ import print_function try: from setuptools import setup diff --git a/tests/integration/dynamodb2/test_highlevel.py b/tests/integration/dynamodb2/test_highlevel.py index 90771422..0f893b14 100644 --- a/tests/integration/dynamodb2/test_highlevel.py +++ b/tests/integration/dynamodb2/test_highlevel.py @@ -23,8 +23,6 @@ """ Tests for DynamoDB v2 high-level abstractions. """ -from __future__ import with_statement - import os import time diff --git a/tests/integration/glacier/test_layer1.py b/tests/integration/glacier/test_layer1.py index effb5628..0d38da27 100644 --- a/tests/integration/glacier/test_layer1.py +++ b/tests/integration/glacier/test_layer1.py @@ -36,7 +36,7 @@ class TestGlacierLayer1(unittest.TestCase): glacier = Layer1() glacier.create_vault('l1testvault') self.addCleanup(glacier.delete_vault, 'l1testvault') - upload_id = glacier.initiate_multipart_upload('l1testvault', 4*1024*1024, + upload_id = glacier.initiate_multipart_upload('l1testvault', 4 * 1024 * 1024, 'double spaces here')['UploadId'] self.addCleanup(glacier.abort_multipart_upload, 'l1testvault', upload_id) response = glacier.list_multipart_uploads('l1testvault')['UploadsList'] diff --git a/tests/integration/s3/mock_storage_service.py b/tests/integration/s3/mock_storage_service.py index d7c59930..8b5ff28d 100644 --- a/tests/integration/s3/mock_storage_service.py +++ b/tests/integration/s3/mock_storage_service.py @@ -30,6 +30,7 @@ import copy import boto import base64 import re +from hashlib import md5 from boto.utils import compute_md5 from boto.utils import find_matching_headers @@ -37,11 +38,6 @@ from boto.utils import merge_headers_by_name from boto.s3.prefix import Prefix from boto.compat import six -try: - from hashlib import md5 -except ImportError: - from md5 import md5 - NOT_IMPL = None diff --git a/tests/integration/ses/test_connection.py b/tests/integration/ses/test_connection.py index f1d66e8c..83b99944 100644 --- a/tests/integration/ses/test_connection.py +++ b/tests/integration/ses/test_connection.py @@ -1,5 +1,3 @@ -from __future__ import with_statement - from tests.unit import unittest from boto.ses.connection import SESConnection diff --git a/tests/integration/sns/test_connection.py b/tests/integration/sns/test_connection.py index e5af487e..6a359b1b 100644 --- a/tests/integration/sns/test_connection.py +++ b/tests/integration/sns/test_connection.py @@ -19,8 +19,6 @@ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. -from __future__ import with_statement - from tests.compat import mock, unittest from boto.compat import http_client from boto.sns import connect_to_region diff --git a/tests/integration/sqs/test_bigmessage.py b/tests/integration/sqs/test_bigmessage.py index ce1e983d..bb52dde1 100644 --- a/tests/integration/sqs/test_bigmessage.py +++ b/tests/integration/sqs/test_bigmessage.py @@ -24,8 +24,6 @@ """ Some unit tests for the SQSConnection """ -from __future__ import with_statement - import time from threading import Timer from tests.unit import unittest @@ -42,7 +40,7 @@ class TestBigMessage(unittest.TestCase): def test_1_basic(self): c = boto.connect_sqs() - + # create a queue so we can test BigMessage queue_name = 'test%d' % int(time.time()) timeout = 60 @@ -61,7 +59,7 @@ class TestBigMessage(unittest.TestCase): fp = StringIO(msg_body) s3_url = 's3://%s' % queue_name message = queue.new_message(fp, s3_url=s3_url) - + queue.write(message) time.sleep(30) @@ -69,7 +67,7 @@ class TestBigMessage(unittest.TestCase): # Make sure msg body is in bucket self.assertTrue(bucket.lookup(s3_object_name)) - + m = queue.read() self.assertEqual(m.get_body().decode('utf-8'), msg_body) diff --git a/tests/unit/auth/test_sigv4.py b/tests/unit/auth/test_sigv4.py index 8c16ebd0..674ec0a7 100644 --- a/tests/unit/auth/test_sigv4.py +++ b/tests/unit/auth/test_sigv4.py @@ -20,6 +20,7 @@ # IN THE SOFTWARE. # import copy +import pickle import os from tests.compat import unittest, mock from tests.unit import MockServiceWithConfigTestCase @@ -29,6 +30,7 @@ from boto.auth import S3HmacAuthV4Handler from boto.auth import detect_potential_s3sigv4 from boto.auth import detect_potential_sigv4 from boto.connection import HTTPRequest +from boto.provider import Provider from boto.regioninfo import RegionInfo @@ -237,6 +239,18 @@ class TestSigV4Handler(unittest.TestCase): scope = auth.credential_scope(self.request) self.assertEqual(scope, '20121121/us-west-2/sqs/aws4_request') + def test_pickle_works(self): + provider = Provider('aws', access_key='access_key', + secret_key='secret_key') + auth = HmacAuthV4Handler('queue.amazonaws.com', None, provider) + + # Pickle it! + pickled = pickle.dumps(auth) + + # Now restore it + auth2 = pickle.loads(pickled) + self.assertEqual(auth.host, auth2.host) + class TestS3HmacAuthV4Handler(unittest.TestCase): def setUp(self): diff --git a/tests/unit/cloudsearch/test_document.py b/tests/unit/cloudsearch/test_document.py index 34c3cad2..929b62be 100644 --- a/tests/unit/cloudsearch/test_document.py +++ b/tests/unit/cloudsearch/test_document.py @@ -8,7 +8,7 @@ import json from boto.cloudsearch.document import DocumentServiceConnection from boto.cloudsearch.document import CommitMismatchError, EncodingError, \ - ContentTooLongError, DocumentServiceConnection + ContentTooLongError, DocumentServiceConnection, SearchServiceException import boto @@ -321,3 +321,17 @@ class CloudSearchDocumentErrorMismatch(CloudSearchDocumentTest): "category": ["cat_a", "cat_b", "cat_c"]}) self.assertRaises(CommitMismatchError, document.commit) + +class CloudSearchDocumentsErrorMissingAdds(CloudSearchDocumentTest): + response = { + 'status': 'error', + 'deletes': 0, + 'errors': [{'message': 'Unknown error message'}] + } + + def test_fake_failure(self): + document = DocumentServiceConnection( + endpoint="doc-demo-userdomain.us-east-1.cloudsearch.amazonaws.com") + document.add("1234", 10, {"id": "1234", "title": "Title 1", + "category": ["cat_a", "cat_b", "cat_c"]}) + self.assertRaises(SearchServiceException, document.commit) diff --git a/tests/unit/dynamodb/test_types.py b/tests/unit/dynamodb/test_types.py index f4f7fb54..25b3f78f 100644 --- a/tests/unit/dynamodb/test_types.py +++ b/tests/unit/dynamodb/test_types.py @@ -86,6 +86,12 @@ class TestBinary(unittest.TestCase): self.assertEqual(b'\x01', data) self.assertEqual(b'\x01', bytes(data)) + def test_non_ascii_good_input(self): + # Binary data that is out of ASCII range + data = types.Binary(b'\x88') + self.assertEqual(b'\x88', data) + self.assertEqual(b'\x88', bytes(data)) + @unittest.skipUnless(six.PY2, "Python 2 only") def test_bad_input(self): with self.assertRaises(TypeError): @@ -108,6 +114,9 @@ class TestBinary(unittest.TestCase): # In Python 2.x these are considered equal self.assertEqual(data, u'\x01') + # Check that the value field is of type bytes + self.assertEqual(type(data.value), bytes) + @unittest.skipUnless(six.PY3, "Python 3 only") def test_unicode_py3(self): with self.assertRaises(TypeError): diff --git a/tests/unit/emr/test_connection.py b/tests/unit/emr/test_connection.py index 510f0c1f..c60f04a4 100644 --- a/tests/unit/emr/test_connection.py +++ b/tests/unit/emr/test_connection.py @@ -19,8 +19,6 @@ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. -from __future__ import with_statement - import boto.utils from datetime import datetime @@ -374,7 +372,74 @@ class TestListSteps(AWSMockServiceTestCase): connection_class = EmrConnection def default_body(self): - return b"""<ListStepsOutput><Steps><member><Name>Step 1</Name></member></Steps></ListStepsOutput>""" + return b"""<ListStepsResponse xmlns="http://elasticmapreduce.amazonaws.com/doc/2009-03-31"> + <ListStepsResult> + <Steps> + <member> + <Id>abc123</Id> + <Status> + <StateChangeReason/> + <Timeline> + <CreationDateTime>2014-07-01T00:00:00.000Z</CreationDateTime> + </Timeline> + <State>PENDING</State> + </Status> + <Name>Step 1</Name> + <Config> + <Jar>/home/hadoop/lib/emr-s3distcp-1.0.jar</Jar> + <Args> + <member>--src</member> + <member>hdfs:///data/test/</member> + <member>--dest</member> + <member>s3n://test/data</member> + </Args> + <Properties/> + </Config> + <ActionOnFailure>CONTINUE</ActionOnFailure> + </member> + <member> + <Id>def456</Id> + <Status> + <StateChangeReason/> + <Timeline> + <CreationDateTime>2014-07-01T00:00:00.000Z</CreationDateTime> + </Timeline> + <State>COMPLETED</State> + </Status> + <Name>Step 2</Name> + <Config> + <MainClass>my.main.SomeClass</MainClass> + <Jar>s3n://test/jars/foo.jar</Jar> + </Config> + <ActionOnFailure>CONTINUE</ActionOnFailure> + </member> + <member> + <Id>ghi789</Id> + <Status> + <StateChangeReason/> + <Timeline> + <CreationDateTime>2014-07-01T00:00:00.000Z</CreationDateTime> + </Timeline> + <State>FAILED</State> + </Status> + <Name>Step 3</Name> + <Config> + <Jar>s3n://test/jars/bar.jar</Jar> + <Args> + <member>-arg</member> + <member>value</member> + </Args> + <Properties/> + </Config> + <ActionOnFailure>TERMINATE_CLUSTER</ActionOnFailure> + </member> + </Steps> + </ListStepsResult> + <ResponseMetadata> + <RequestId>eff31ee5-0342-11e4-b3c7-9de5a93f6fcb</RequestId> + </ResponseMetadata> +</ListStepsResponse> +""" def test_list_steps(self): self.set_http_response(200) @@ -392,6 +457,30 @@ class TestListSteps(AWSMockServiceTestCase): self.assertTrue(isinstance(response, StepSummaryList)) self.assertEqual(response.steps[0].name, 'Step 1') + valid_states = [ + 'PENDING', + 'RUNNING', + 'COMPLETED', + 'CANCELLED', + 'FAILED', + 'INTERRUPTED' + ] + + # Check for step states + for step in response.steps: + self.assertIn(step.status.state, valid_states) + + # Check for step config + step = response.steps[0] + self.assertEqual(step.config.jar, + '/home/hadoop/lib/emr-s3distcp-1.0.jar') + self.assertEqual(len(step.config.args), 4) + self.assertEqual(step.config.args[0].value, '--src') + self.assertEqual(step.config.args[1].value, 'hdfs:///data/test/') + + step = response.steps[1] + self.assertEqual(step.config.mainclass, 'my.main.SomeClass') + def test_list_steps_with_states(self): self.set_http_response(200) response = self.service_connection.list_steps( diff --git a/tests/unit/glacier/test_job.py b/tests/unit/glacier/test_job.py index ac47ad8d..c7b7b1fb 100644 --- a/tests/unit/glacier/test_job.py +++ b/tests/unit/glacier/test_job.py @@ -56,7 +56,7 @@ class TestJob(unittest.TestCase): self.job.get_output(byte_range=(1, 1024), validate_checksum=False) def test_download_to_fileobj(self): - http_response=mock.Mock(read=mock.Mock(return_value='xyz')) + http_response = mock.Mock(read=mock.Mock(return_value='xyz')) response = GlacierResponse(http_response, None) response['TreeHash'] = 'tree_hash' self.api.get_job_output.return_value = response diff --git a/tests/unit/glacier/test_layer1.py b/tests/unit/glacier/test_layer1.py index 1e6490bf..4c8f0cf7 100644 --- a/tests/unit/glacier/test_layer1.py +++ b/tests/unit/glacier/test_layer1.py @@ -76,7 +76,7 @@ class GlacierJobOperations(GlacierLayer1ConnectionBase): self.set_http_response(status_code=200, header=header, body=self.job_content) response = self.service_connection.get_job_output(self.vault_name, - 'example-job-id') + 'example-job-id') self.assertEqual(self.job_content, response.read()) diff --git a/tests/unit/glacier/test_layer2.py b/tests/unit/glacier/test_layer2.py index eec175d3..84b53aac 100644 --- a/tests/unit/glacier/test_layer2.py +++ b/tests/unit/glacier/test_layer2.py @@ -32,73 +32,71 @@ import boto.glacier.vault from boto.glacier.vault import Vault from boto.glacier.vault import Job -from boto.compat import StringIO - from datetime import datetime, tzinfo, timedelta # Some fixture data from the Glacier docs FIXTURE_VAULT = { - "CreationDate" : "2012-02-20T17:01:45.198Z", - "LastInventoryDate" : "2012-03-20T17:03:43.221Z", - "NumberOfArchives" : 192, - "SizeInBytes" : 78088912, - "VaultARN" : "arn:aws:glacier:us-east-1:012345678901:vaults/examplevault", - "VaultName" : "examplevault" + "CreationDate": "2012-02-20T17:01:45.198Z", + "LastInventoryDate": "2012-03-20T17:03:43.221Z", + "NumberOfArchives": 192, + "SizeInBytes": 78088912, + "VaultARN": "arn:aws:glacier:us-east-1:012345678901:vaults/examplevault", + "VaultName": "examplevault" } FIXTURE_VAULTS = { - 'RequestId': 'vuXO7SHTw-luynJ0Zu31AYjR3TcCn7X25r7ykpuulxY2lv8', - 'VaultList': [{'SizeInBytes': 0, 'LastInventoryDate': None, - 'VaultARN': 'arn:aws:glacier:us-east-1:686406519478:vaults/vault0', - 'VaultName': 'vault0', 'NumberOfArchives': 0, - 'CreationDate': '2013-05-17T02:38:39.049Z'}, - {'SizeInBytes': 0, 'LastInventoryDate': None, - 'VaultARN': 'arn:aws:glacier:us-east-1:686406519478:vaults/vault3', - 'VaultName': 'vault3', 'NumberOfArchives': 0, - 'CreationDate': '2013-05-17T02:31:18.659Z'}]} + 'RequestId': 'vuXO7SHTw-luynJ0Zu31AYjR3TcCn7X25r7ykpuulxY2lv8', + 'VaultList': [{'SizeInBytes': 0, 'LastInventoryDate': None, + 'VaultARN': 'arn:aws:glacier:us-east-1:686406519478:vaults/vault0', + 'VaultName': 'vault0', 'NumberOfArchives': 0, + 'CreationDate': '2013-05-17T02:38:39.049Z'}, + {'SizeInBytes': 0, 'LastInventoryDate': None, + 'VaultARN': 'arn:aws:glacier:us-east-1:686406519478:vaults/vault3', + 'VaultName': 'vault3', 'NumberOfArchives': 0, + 'CreationDate': '2013-05-17T02:31:18.659Z'}]} FIXTURE_PAGINATED_VAULTS = { - 'Marker': 'arn:aws:glacier:us-east-1:686406519478:vaults/vault2', - 'RequestId': 'vuXO7SHTw-luynJ0Zu31AYjR3TcCn7X25r7ykpuulxY2lv8', - 'VaultList': [{'SizeInBytes': 0, 'LastInventoryDate': None, - 'VaultARN': 'arn:aws:glacier:us-east-1:686406519478:vaults/vault0', - 'VaultName': 'vault0', 'NumberOfArchives': 0, - 'CreationDate': '2013-05-17T02:38:39.049Z'}, - {'SizeInBytes': 0, 'LastInventoryDate': None, - 'VaultARN': 'arn:aws:glacier:us-east-1:686406519478:vaults/vault1', - 'VaultName': 'vault1', 'NumberOfArchives': 0, - 'CreationDate': '2013-05-17T02:31:18.659Z'}]} + 'Marker': 'arn:aws:glacier:us-east-1:686406519478:vaults/vault2', + 'RequestId': 'vuXO7SHTw-luynJ0Zu31AYjR3TcCn7X25r7ykpuulxY2lv8', + 'VaultList': [{'SizeInBytes': 0, 'LastInventoryDate': None, + 'VaultARN': 'arn:aws:glacier:us-east-1:686406519478:vaults/vault0', + 'VaultName': 'vault0', 'NumberOfArchives': 0, + 'CreationDate': '2013-05-17T02:38:39.049Z'}, + {'SizeInBytes': 0, 'LastInventoryDate': None, + 'VaultARN': 'arn:aws:glacier:us-east-1:686406519478:vaults/vault1', + 'VaultName': 'vault1', 'NumberOfArchives': 0, + 'CreationDate': '2013-05-17T02:31:18.659Z'}]} FIXTURE_PAGINATED_VAULTS_CONT = { - 'Marker': None, - 'RequestId': 'vuXO7SHTw-luynJ0Zu31AYjR3TcCn7X25r7ykpuulxY2lv8', - 'VaultList': [{'SizeInBytes': 0, 'LastInventoryDate': None, - 'VaultARN': 'arn:aws:glacier:us-east-1:686406519478:vaults/vault2', - 'VaultName': 'vault2', 'NumberOfArchives': 0, - 'CreationDate': '2013-05-17T02:38:39.049Z'}, - {'SizeInBytes': 0, 'LastInventoryDate': None, - 'VaultARN': 'arn:aws:glacier:us-east-1:686406519478:vaults/vault3', - 'VaultName': 'vault3', 'NumberOfArchives': 0, - 'CreationDate': '2013-05-17T02:31:18.659Z'}]} + 'Marker': None, + 'RequestId': 'vuXO7SHTw-luynJ0Zu31AYjR3TcCn7X25r7ykpuulxY2lv8', + 'VaultList': [{'SizeInBytes': 0, 'LastInventoryDate': None, + 'VaultARN': 'arn:aws:glacier:us-east-1:686406519478:vaults/vault2', + 'VaultName': 'vault2', 'NumberOfArchives': 0, + 'CreationDate': '2013-05-17T02:38:39.049Z'}, + {'SizeInBytes': 0, 'LastInventoryDate': None, + 'VaultARN': 'arn:aws:glacier:us-east-1:686406519478:vaults/vault3', + 'VaultName': 'vault3', 'NumberOfArchives': 0, + 'CreationDate': '2013-05-17T02:31:18.659Z'}]} FIXTURE_ARCHIVE_JOB = { - "Action": "ArchiveRetrieval", - "ArchiveId": ("NkbByEejwEggmBz2fTHgJrg0XBoDfjP4q6iu87-TjhqG6eGoOY9Z8i1_AUyUs" - "uhPAdTqLHy8pTl5nfCFJmDl2yEZONi5L26Omw12vcs01MNGntHEQL8MBfGlqr" - "EXAMPLEArchiveId"), - "ArchiveSizeInBytes": 16777216, - "Completed": False, - "CreationDate": "2012-05-15T17:21:39.339Z", - "CompletionDate": "2012-05-15T17:21:43.561Z", - "InventorySizeInBytes": None, - "JobDescription": "My ArchiveRetrieval Job", - "JobId": ("HkF9p6o7yjhFx-K3CGl6fuSm6VzW9T7esGQfco8nUXVYwS0jlb5gq1JZ55yHgt5v" - "P54ZShjoQzQVVh7vEXAMPLEjobID"), - "SHA256TreeHash": ("beb0fe31a1c7ca8c6c04d574ea906e3f97b31fdca7571defb5b44dc" - "a89b5af60"), - "SNSTopic": "arn:aws:sns:us-east-1:012345678901:mytopic", - "StatusCode": "InProgress", - "StatusMessage": "Operation in progress.", - "VaultARN": "arn:aws:glacier:us-east-1:012345678901:vaults/examplevault" + "Action": "ArchiveRetrieval", + "ArchiveId": ("NkbByEejwEggmBz2fTHgJrg0XBoDfjP4q6iu87-TjhqG6eGoOY9Z8i1_AUyUs" + "uhPAdTqLHy8pTl5nfCFJmDl2yEZONi5L26Omw12vcs01MNGntHEQL8MBfGlqr" + "EXAMPLEArchiveId"), + "ArchiveSizeInBytes": 16777216, + "Completed": False, + "CreationDate": "2012-05-15T17:21:39.339Z", + "CompletionDate": "2012-05-15T17:21:43.561Z", + "InventorySizeInBytes": None, + "JobDescription": "My ArchiveRetrieval Job", + "JobId": ("HkF9p6o7yjhFx-K3CGl6fuSm6VzW9T7esGQfco8nUXVYwS0jlb5gq1JZ55yHgt5v" + "P54ZShjoQzQVVh7vEXAMPLEjobID"), + "SHA256TreeHash": ("beb0fe31a1c7ca8c6c04d574ea906e3f97b31fdca7571defb5b44dc" + "a89b5af60"), + "SNSTopic": "arn:aws:sns:us-east-1:012345678901:mytopic", + "StatusCode": "InProgress", + "StatusMessage": "Operation in progress.", + "VaultARN": "arn:aws:glacier:us-east-1:012345678901:vaults/examplevault" } EXAMPLE_PART_LIST_RESULT_PAGE_1 = { @@ -107,11 +105,10 @@ EXAMPLE_PART_LIST_RESULT_PAGE_1 = { "Marker": "MfgsKHVjbQ6EldVl72bn3_n5h2TaGZQUO-Qb3B9j3TITf7WajQ", "MultipartUploadId": "OW2fM5iVylEpFEMM9_HpKowRapC3vn5sSL39_396UW9zLFUWVrnRHaPjUJddQ5OxSHVXjYtrN47NBZ-khxOjyEXAMPLE", "PartSizeInBytes": 4194304, - "Parts": - [ { - "RangeInBytes": "4194304-8388607", - "SHA256TreeHash": "01d34dabf7be316472c93b1ef80721f5d4" - }], + "Parts": [{ + "RangeInBytes": "4194304-8388607", + "SHA256TreeHash": "01d34dabf7be316472c93b1ef80721f5d4" + }], "VaultARN": "arn:aws:glacier:us-east-1:012345678901:vaults/demo1-vault" } @@ -123,11 +120,10 @@ EXAMPLE_PART_LIST_RESULT_PAGE_2 = { "Marker": None, "MultipartUploadId": None, "PartSizeInBytes": None, - "Parts": - [ { - "RangeInBytes": "0-4194303", - "SHA256TreeHash": "01d34dabf7be316472c93b1ef80721f5d4" - }], + "Parts": [{ + "RangeInBytes": "0-4194303", + "SHA256TreeHash": "01d34dabf7be316472c93b1ef80721f5d4" + }], "VaultARN": None } @@ -137,14 +133,13 @@ EXAMPLE_PART_LIST_COMPLETE = { "Marker": None, "MultipartUploadId": "OW2fM5iVylEpFEMM9_HpKowRapC3vn5sSL39_396UW9zLFUWVrnRHaPjUJddQ5OxSHVXjYtrN47NBZ-khxOjyEXAMPLE", "PartSizeInBytes": 4194304, - "Parts": - [ { - "RangeInBytes": "4194304-8388607", - "SHA256TreeHash": "01d34dabf7be316472c93b1ef80721f5d4" + "Parts": [{ + "RangeInBytes": "4194304-8388607", + "SHA256TreeHash": "01d34dabf7be316472c93b1ef80721f5d4" }, { - "RangeInBytes": "0-4194303", - "SHA256TreeHash": "01d34dabf7be316472c93b1ef80721f5d4" - }], + "RangeInBytes": "0-4194303", + "SHA256TreeHash": "01d34dabf7be316472c93b1ef80721f5d4" + }], "VaultARN": "arn:aws:glacier:us-east-1:012345678901:vaults/demo1-vault" } @@ -183,7 +178,7 @@ class TestGlacierLayer2Connection(GlacierLayer2Base): def return_paginated_vaults_resp(marker=None, limit=None): return resps.pop(0) - self.mock_layer1.list_vaults = Mock(side_effect = return_paginated_vaults_resp) + self.mock_layer1.list_vaults = Mock(side_effect=return_paginated_vaults_resp) vaults = self.layer2.list_vaults() self.assertEqual(vaults[0].name, "vault0") self.assertEqual(vaults[3].name, "vault3") @@ -287,11 +282,11 @@ class TestVault(GlacierLayer2Base): 'Parts': [{ 'RangeInBytes': '0-3', 'SHA256TreeHash': '12', - }, { + }, { 'RangeInBytes': '4-6', 'SHA256TreeHash': '34', - }, - ]} + }], + } self.vault.list_all_parts = mock_list_parts self.vault.resume_archive_from_file( @@ -315,6 +310,7 @@ class TestJob(GlacierLayer2Base): "HkF9p6o7yjhFx-K3CGl6fuSm6VzW9T7esGQfco8nUXVYwS0jlb5gq1JZ55yHgt5vP" "54ZShjoQzQVVh7vEXAMPLEjobID", (0, 100)) + class TestRangeStringParsing(unittest.TestCase): def test_simple_range(self): self.assertEquals( diff --git a/tests/unit/glacier/test_utils.py b/tests/unit/glacier/test_utils.py index a051b59e..bace2a38 100644 --- a/tests/unit/glacier/test_utils.py +++ b/tests/unit/glacier/test_utils.py @@ -19,13 +19,16 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # -import time import logging +import os +import tempfile +import time from hashlib import sha256 from tests.unit import unittest +from boto.compat import BytesIO, six, StringIO from boto.glacier.utils import minimum_part_size, chunk_hashes, tree_hash, \ - bytes_to_hex + bytes_to_hex, compute_hashes_from_fileobj class TestPartSizeCalculations(unittest.TestCase): @@ -114,3 +117,49 @@ class TestTreeHash(unittest.TestCase): self.assertEqual( self.calculate_tree_hash(''), b'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') + + +class TestFileHash(unittest.TestCase): + def _gen_data(self): + # Generate some pseudo-random bytes of data. We include the + # hard-coded blob as an example that fails to decode via UTF-8. + return os.urandom(5000) + b'\xc2\x00' + + def test_compute_hash_tempfile(self): + # Compute a hash from a file object. On Python 2 this uses a non- + # binary mode. On Python 3, however, binary mode is required for + # binary files. If not used, you will get UTF-8 code errors. + if six.PY2: + mode = "w+" + else: + mode = "wb+" + + with tempfile.TemporaryFile(mode=mode) as f: + f.write(self._gen_data()) + f.seek(0) + + compute_hashes_from_fileobj(f, chunk_size=512) + + @unittest.skipUnless(six.PY3, 'Python 3 requires reading binary!') + def test_compute_hash_tempfile_py3(self): + # Note the missing 'b' in the mode! + with tempfile.TemporaryFile(mode='w+') as f: + with self.assertRaises(ValueError): + compute_hashes_from_fileobj(f, chunk_size=512) + + # What about file-like objects without a mode? If it has an + # encoding we use it, otherwise attempt UTF-8 encoding to + # bytes for hashing. + f = StringIO('test data' * 500) + compute_hashes_from_fileobj(f, chunk_size=512) + + @unittest.skipUnless(six.PY2, 'Python 3 requires reading binary!') + def test_compute_hash_stringio(self): + # Python 2 binary data in StringIO example + f = StringIO(self._gen_data()) + compute_hashes_from_fileobj(f, chunk_size=512) + + def test_compute_hash_bytesio(self): + # Compute a hash from a file-like BytesIO object. + f = BytesIO(self._gen_data()) + compute_hashes_from_fileobj(f, chunk_size=512) diff --git a/tests/unit/glacier/test_vault.py b/tests/unit/glacier/test_vault.py index 68d9d784..f532e3b9 100644 --- a/tests/unit/glacier/test_vault.py +++ b/tests/unit/glacier/test_vault.py @@ -44,7 +44,9 @@ class TestVault(unittest.TestCase): def tearDown(self): self.size_patch.stop() - def test_upload_archive_small_file(self): + @mock.patch('boto.glacier.vault.compute_hashes_from_fileobj', + return_value=[b'abc', b'123']) + def test_upload_archive_small_file(self, compute_hashes): self.getsize.return_value = 1 self.api.upload_archive.return_value = {'ArchiveId': 'archive_id'} @@ -69,7 +71,7 @@ class TestVault(unittest.TestCase): # The write should be created with the default part size of the # instance (2 MB). self.vault.create_archive_writer.assert_called_with( - description=mock.ANY, part_size=self.vault.DefaultPartSize) + description=mock.ANY, part_size=self.vault.DefaultPartSize) def test_large_part_size_is_obeyed(self): self.vault.DefaultPartSize = 8 * 1024 * 1024 diff --git a/tests/unit/glacier/test_writer.py b/tests/unit/glacier/test_writer.py index c7066b97..b2875f3c 100644 --- a/tests/unit/glacier/test_writer.py +++ b/tests/unit/glacier/test_writer.py @@ -27,7 +27,6 @@ from tests.unit import unittest from mock import ( call, Mock, - patch, sentinel, ) from nose.tools import assert_equal @@ -50,7 +49,7 @@ def create_mock_vault(): def partify(data, part_size): for i in itertools.count(0): start = i * part_size - part = data[start:start+part_size] + part = data[start:start + part_size] if part: yield part else: diff --git a/tests/unit/s3/test_key.py b/tests/unit/s3/test_key.py index ee8b686a..7752d9cd 100644 --- a/tests/unit/s3/test_key.py +++ b/tests/unit/s3/test_key.py @@ -20,8 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # -from __future__ import with_statement - from tests.compat import mock, unittest from tests.unit import AWSMockServiceTestCase diff --git a/tests/unit/test_connection.py b/tests/unit/test_connection.py index 5d726e06..3d4a57b3 100644 --- a/tests/unit/test_connection.py +++ b/tests/unit/test_connection.py @@ -19,8 +19,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # -from __future__ import with_statement - import os import socket @@ -116,7 +114,7 @@ class TestAWSAuthConnection(unittest.TestCase): self.assertEqual(conn.get_path('/folder//image.jpg'), '/folder//image.jpg') self.assertEqual(conn.get_path('/folder////image.jpg'), '/folder////image.jpg') self.assertEqual(conn.get_path('///folder////image.jpg'), '///folder////image.jpg') - + def test_connection_behind_proxy(self): os.environ['http_proxy'] = "http://john.doe:p4ssw0rd@127.0.0.1:8180" conn = AWSAuthConnection( @@ -130,7 +128,7 @@ class TestAWSAuthConnection(unittest.TestCase): self.assertEqual(conn.proxy_pass, 'p4ssw0rd') self.assertEqual(conn.proxy_port, '8180') del os.environ['http_proxy'] - + def test_connection_behind_proxy_without_explicit_port(self): os.environ['http_proxy'] = "http://127.0.0.1" conn = AWSAuthConnection( @@ -139,7 +137,7 @@ class TestAWSAuthConnection(unittest.TestCase): aws_secret_access_key='secret', suppress_consec_slashes=False, port=8180 - ) + ) self.assertEqual(conn.proxy, '127.0.0.1') self.assertEqual(conn.proxy_port, 8180) del os.environ['http_proxy'] diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index 2e7ddbd3..57a04a66 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -207,6 +207,22 @@ class TestRetryURL(unittest.TestCase): response = retry_url('http://10.10.10.10/foo', num_retries=1) self.assertEqual(response, 'no proxy response') + def test_retry_url_using_bytes_and_string_response(self): + test_value = 'normal response' + fake_response = mock.Mock() + + # test using unicode + fake_response.read.return_value = test_value + self.opener.return_value.open.return_value = fake_response + response = retry_url('http://10.10.10.10/foo', num_retries=1) + self.assertEqual(response, test_value) + + # test using bytes + fake_response.read.return_value = test_value.encode('utf-8') + self.opener.return_value.open.return_value = fake_response + response = retry_url('http://10.10.10.10/foo', num_retries=1) + self.assertEqual(response, test_value) + class TestLazyLoadMetadata(unittest.TestCase): def setUp(self): diff --git a/tests/unit/vpc/test_vpc_peering_connection.py b/tests/unit/vpc/test_vpc_peering_connection.py index 42e68d44..503e5606 100644 --- a/tests/unit/vpc/test_vpc_peering_connection.py +++ b/tests/unit/vpc/test_vpc_peering_connection.py @@ -19,7 +19,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. -from tests.unit import unittest +from tests.unit import mock, unittest from tests.unit import AWSMockServiceTestCase from boto.vpc import VpcPeeringConnection, VPCConnection, Subnet @@ -159,6 +159,58 @@ class TestDeleteVpcPeeringConnection(AWSMockServiceTestCase): self.set_http_response(status_code=200) self.assertEquals(self.service_connection.delete_vpc_peering_connection('pcx-12345678'), True) +class TestDeleteVpcPeeringConnectionShortForm(unittest.TestCase): + DESCRIBE_VPC_PEERING_CONNECTIONS= b"""<?xml version="1.0" encoding="UTF-8"?> +<DescribeVpcPeeringConnectionsResponse xmlns="http://ec2.amazonaws.com/doc/2014-05-01/"> + <requestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</requestId> + <vpcPeeringConnectionSet> + <item> + <vpcPeeringConnectionId>pcx-111aaa22</vpcPeeringConnectionId> + <requesterVpcInfo> + <ownerId>777788889999</ownerId> + <vpcId>vpc-1a2b3c4d</vpcId> + <cidrBlock>172.31.0.0/16</cidrBlock> + </requesterVpcInfo> + <accepterVpcInfo> + <ownerId>111122223333</ownerId> + <vpcId>vpc-aa22cc33</vpcId> + </accepterVpcInfo> + <status> + <code>pending-acceptance</code> + <message>Pending Acceptance by 111122223333</message> + </status> + <expirationTime>2014-02-17T16:00:50.000Z</expirationTime> + </item> + </vpcPeeringConnectionSet> +</DescribeVpcPeeringConnectionsResponse>""" + + DELETE_VPC_PEERING_CONNECTION= b"""<DeleteVpcPeeringConnectionResponse xmlns="http://ec2.amazonaws.com/doc/2014-05-01/"> + <requestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</requestId> + <return>true</return> +</DeleteVpcPeeringConnectionResponse>""" + + def test_delete_vpc_peering_connection(self): + vpc_conn = VPCConnection(aws_access_key_id='aws_access_key_id', + aws_secret_access_key='aws_secret_access_key') + + mock_response = mock.Mock() + mock_response.read.return_value = self.DESCRIBE_VPC_PEERING_CONNECTIONS + mock_response.status = 200 + vpc_conn.make_request = mock.Mock(return_value=mock_response) + vpc_peering_connections = vpc_conn.get_all_vpc_peering_connections() + + self.assertEquals(1, len(vpc_peering_connections)) + vpc_peering_connection = vpc_peering_connections[0] + + mock_response = mock.Mock() + mock_response.read.return_value = self.DELETE_VPC_PEERING_CONNECTION + mock_response.status = 200 + vpc_conn.make_request = mock.Mock(return_value=mock_response) + self.assertEquals(True, vpc_peering_connection.delete()) + + self.assertIn('DeleteVpcPeeringConnection', vpc_conn.make_request.call_args_list[0][0]) + self.assertNotIn('DeleteVpc', vpc_conn.make_request.call_args_list[0][0]) + class TestRejectVpcPeeringConnection(AWSMockServiceTestCase): REJECT_VPC_PEERING_CONNECTION= b"""<RejectVpcPeeringConnectionResponse xmlns="http://ec2.amazonaws.com/doc/2014-05-01/"> <requestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</requestId> @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,py33,py34 +envlist = py26,py27,py33,py34,pypy # Comment to build sdist and install into virtualenv # This is helpful to test installation but takes extra time @@ -10,7 +10,6 @@ skipsdist = True deps = unittest2 ordereddict - mock==1.0.1 -rrequirements.txt # Some tests expect specific ordering, so we set the hash # seed for Python 2.x until all tests are updated for Python 3. @@ -22,12 +21,16 @@ setenv = [testenv:py27] deps = - mock==1.0.1 -rrequirements.txt # See comment above in py26 about hash seed. setenv = PYTHONHASHSEED = 0 +[testenv:pypy] +# See comment above in py26 about hash seed. +setenv = + PYTHONHASHSEED = 0 + [testenv] deps = -rrequirements.txt commands = |