diff --git a/.travis.yml b/.travis.yml index 4a12ed2..c4ca47c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,5 +2,7 @@ language: python python: - 2.6 - 2.7 + - 3.3 + - 3.4 install: pip install versiontools --use-mirrors script: python setup.py test diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..64370a3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,203 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +--- License for python-keystoneclient versions prior to 2.1 --- + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of this project nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/chef/api.py b/chef/api.py index 4d8bc75..b79ffd8 100644 --- a/chef/api.py +++ b/chef/api.py @@ -1,14 +1,14 @@ -import copy +import six import datetime -import itertools import logging import os import re import socket import subprocess import threading -import urllib2 -import urlparse +import six.moves.urllib.request +import six.moves.urllib.error +import six.moves.urllib.parse import weakref import pkg_resources @@ -38,17 +38,17 @@ class UnknownRubyExpression(Exception): """Token exception for unprocessed Ruby expressions.""" -class ChefRequest(urllib2.Request): +class ChefRequest(six.moves.urllib.request.Request): """Workaround for using PUT/DELETE with urllib2.""" def __init__(self, *args, **kwargs): self._method = kwargs.pop('method', None) # Request is an old-style class, no super() allowed. - urllib2.Request.__init__(self, *args, **kwargs) + six.moves.urllib.request.Request.__init__(self, *args, **kwargs) def get_method(self): if self._method: return self._method - return urllib2.Request.get_method(self) + return six.moves.urllib.request.Request.get_method(self) class ChefAPI(object): @@ -72,7 +72,7 @@ class ChefAPI(object): def __init__(self, url, key, client, version='0.10.8', headers={}): self.url = url.rstrip('/') - self.parsed_url = urlparse.urlparse(self.url) + self.parsed_url = six.moves.urllib.parse.urlparse(self.url) if not isinstance(key, Key): key = Key(key) if not key.key: @@ -80,7 +80,7 @@ def __init__(self, url, key, client, version='0.10.8', headers={}): self.key = key self.client = client self.version = version - self.headers = dict((k.lower(), v) for k, v in headers.iteritems()) + self.headers = dict((k.lower(), v) for k, v in six.iteritems(headers)) self.version_parsed = pkg_resources.parse_version(self.version) self.platform = self.parsed_url.hostname == 'api.opscode.com' if not api_stack_value(): @@ -109,7 +109,7 @@ def from_config_file(cls, path): value = md.group(2) else: # Not a string, don't even try - log.debug('Value for %s does not look like a string: %s'%(key, value)) + log.debug('Value for {0} does not look like a string: {1}'.format(key, value)) continue def _ruby_value(match): expr = match.group(1).strip() @@ -193,8 +193,10 @@ def __exit__(self, type, value, traceback): def _request(self, method, url, data, headers): # Testing hook, subclass and override for WSGI intercept + if six.PY3 and data: + data = data.encode() request = ChefRequest(url, data, headers, method=method) - return urllib2.urlopen(request).read() + return six.moves.urllib.request.urlopen(request).read() def request(self, method, path, headers={}, data=None): auth_headers = sign_request(key=self.key, http_method=method, @@ -203,15 +205,16 @@ def request(self, method, path, headers={}, data=None): user_id=self.client) request_headers = {} request_headers.update(self.headers) - request_headers.update(dict((k.lower(), v) for k, v in headers.iteritems())) + request_headers.update(dict((k.lower(), v) for k, v in six.iteritems(headers))) request_headers['x-chef-version'] = self.version request_headers.update(auth_headers) try: - response = self._request(method, self.url+path, data, dict((k.capitalize(), v) for k, v in request_headers.iteritems())) - except urllib2.HTTPError, e: + response = self._request(method, self.url + path, data, dict( + (k.capitalize(), v) for k, v in six.iteritems(request_headers))) + except six.moves.urllib.error.HTTPError as e: e.content = e.read() try: - e.content = json.loads(e.content) + e.content = json.loads(e.content.decode()) raise ChefServerError.from_error(e.content['error'], code=e.code) except ValueError: pass @@ -219,13 +222,13 @@ def request(self, method, path, headers={}, data=None): return response def api_request(self, method, path, headers={}, data=None): - headers = dict((k.lower(), v) for k, v in headers.iteritems()) + headers = dict((k.lower(), v) for k, v in six.iteritems(headers)) headers['accept'] = 'application/json' if data is not None: headers['content-type'] = 'application/json' data = json.dumps(data) response = self.request(method, path, headers, data) - return json.loads(response) + return json.loads(response.decode()) def __getitem__(self, path): return self.api_request('GET', path) diff --git a/chef/auth.py b/chef/auth.py index 494c604..1f1d582 100644 --- a/chef/auth.py +++ b/chef/auth.py @@ -1,3 +1,4 @@ +import six.moves import base64 import datetime import hashlib @@ -8,19 +9,19 @@ def _ruby_b64encode(value): into 60-character chunks. """ b64 = base64.b64encode(value) - for i in xrange(0, len(b64), 60): - yield b64[i:i+60] + for i in six.moves.range(0, len(b64), 60): + yield b64[i:i + 60].decode() def ruby_b64encode(value): return '\n'.join(_ruby_b64encode(value)) def sha1_base64(value): """An implementation of Mixlib::Authentication::Digester.""" - return ruby_b64encode(hashlib.sha1(value).digest()) + return ruby_b64encode(hashlib.sha1(value.encode()).digest()) class UTC(datetime.tzinfo): """UTC timezone stub.""" - + ZERO = datetime.timedelta(0) def utcoffset(self, dt): @@ -63,7 +64,7 @@ def sign_request(key, http_method, path, body, host, timestamp, user_id): """Generate the needed headers for the Opscode authentication protocol.""" timestamp = canonical_time(timestamp) hashed_body = sha1_base64(body or '') - + # Simple headers headers = { 'x-ops-sign': 'version=1.0', @@ -71,7 +72,7 @@ def sign_request(key, http_method, path, body, host, timestamp, user_id): 'x-ops-timestamp': timestamp, 'x-ops-content-hash': hashed_body, } - + # Create RSA signature req = canonical_request(http_method, path, hashed_body, timestamp, user_id) sig = _ruby_b64encode(key.private_encrypt(req)) diff --git a/chef/base.py b/chef/base.py index 300b5b8..bc6d4a6 100644 --- a/chef/base.py +++ b/chef/base.py @@ -1,3 +1,4 @@ +import six import collections import pkg_resources @@ -34,10 +35,8 @@ def __init__(cls, name, bases, d): cls.api_version_parsed = pkg_resources.parse_version(cls.api_version) -class ChefObject(object): +class ChefObject(six.with_metaclass(ChefObjectMeta, object)): """A base class for Chef API objects.""" - - __metaclass__ = ChefObjectMeta types = {} url = '' @@ -63,7 +62,7 @@ def __init__(self, name, api=None, skip_load=False): self._populate(data) def _populate(self, data): - for name, cls in self.__class__.attributes.iteritems(): + for name, cls in six.iteritems(self.__class__.attributes): if name in data: value = cls(data[name]) else: @@ -83,7 +82,7 @@ def list(cls, api=None): """ api = api or ChefAPI.get_global() cls._check_api_version(api) - names = [name for name, url in api[cls.url].iteritems()] + names = [name for name, url in six.iteritems(api[cls.url])] return ChefQuery(cls, names, api) @classmethod @@ -94,7 +93,7 @@ def create(cls, name, api=None, **kwargs): api = api or ChefAPI.get_global() cls._check_api_version(api) obj = cls(name, api, skip_load=True) - for key, value in kwargs.iteritems(): + for key, value in six.iteritems(kwargs): setattr(obj, key, value) api.api_request('POST', cls.url, data=obj) return obj @@ -106,7 +105,7 @@ def save(self, api=None): api = api or self.api try: api.api_request('PUT', self.url, data=self) - except ChefServerNotFoundError, e: + except ChefServerNotFoundError as e: # If you get a 404 during a save, just create it instead # This mirrors the logic in the Chef code api.api_request('POST', self.__class__.url, data=self) @@ -122,7 +121,7 @@ def to_dict(self): 'json_class': 'Chef::'+self.__class__.__name__, 'chef_type': self.__class__.__name__.lower(), } - for attr in self.__class__.attributes.iterkeys(): + for attr in six.iterkeys(self.__class__.attributes): d[attr] = getattr(self, attr) return d @@ -138,4 +137,4 @@ def _check_api_version(cls, api): # use for creating Chef objects without an API connection (just for # serialization perhaps). if api and cls.api_version_parsed > api.version_parsed: - raise ChefAPIVersionError, "Class %s is not compatible with API version %s" % (cls.__name__, api.version) + raise ChefAPIVersionError("Class %s is not compatible with API version %s" % (cls.__name__, api.version)) diff --git a/chef/data_bag.py b/chef/data_bag.py index f96dcd0..0503640 100644 --- a/chef/data_bag.py +++ b/chef/data_bag.py @@ -1,3 +1,4 @@ +import six import abc import collections @@ -9,7 +10,7 @@ class DataBagMeta(ChefObjectMeta, abc.ABCMeta): """A metaclass to allow DataBag to use multiple inheritance.""" -class DataBag(ChefObject, ChefQuery): +class DataBag(six.with_metaclass(DataBagMeta, ChefObject, ChefQuery)): """A Chef data bag object. Data bag items are available via the mapping API. Evaluation works in the @@ -18,29 +19,25 @@ class DataBag(ChefObject, ChefQuery): bag = DataBag('versions') item = bag['web'] - for name, item in bag.iteritems(): + for name, item in six.iteritems(bag): print item['qa_version'] """ - __metaclass__ = DataBagMeta - url = '/data' def _populate(self, data): - self.names = data.keys() + self.names = list(data.keys()) def obj_class(self, name, api): return DataBagItem(self, name, api=api) -class DataBagItem(ChefObject, collections.MutableMapping): +class DataBagItem(six.with_metaclass(DataBagMeta, ChefObject, collections.MutableMapping)): """A Chef data bag item object. Data bag items act as normal dicts and can contain arbitrary data. """ - __metaclass__ = DataBagMeta - url = '/data' attributes = { 'raw_data': dict, @@ -99,7 +96,7 @@ def create(cls, bag, name, api=None, **kwargs): keyword arguments.""" api = api or ChefAPI.get_global() obj = cls(bag, name, api, skip_load=True) - for key, value in kwargs.iteritems(): + for key, value in six.iteritems(kwargs): obj[key] = value obj['id'] = name api.api_request('POST', cls.url+'/'+str(bag), data=obj.raw_data) @@ -117,5 +114,5 @@ def save(self, api=None): self['id'] = self.name try: api.api_request('PUT', self.url, data=self.raw_data) - except ChefServerNotFoundError, e: + except ChefServerNotFoundError as e: api.api_request('POST', self.__class__.url+'/'+str(self._bag), data=self.raw_data) diff --git a/chef/exceptions.py b/chef/exceptions.py index 9b7de67..63a4987 100644 --- a/chef/exceptions.py +++ b/chef/exceptions.py @@ -11,7 +11,7 @@ class ChefServerError(ChefError): def __init__(self, message, code=None): self.raw_message = message if isinstance(message, list): - message = u', '.join(m for m in message if m) + message = ', '.join(m for m in message if m) super(ChefError, self).__init__(message) self.code = code diff --git a/chef/fabric.py b/chef/fabric.py index 178be41..8d49e25 100644 --- a/chef/fabric.py +++ b/chef/fabric.py @@ -1,5 +1,4 @@ -from __future__ import absolute_import - +import six import functools from chef.api import ChefAPI, autoconfigure @@ -9,7 +8,7 @@ try: from fabric.api import env, task, roles, output -except ImportError, e: +except ImportError as e: env = {} task = lambda *args, **kwargs: lambda fn: fn roles = task @@ -38,7 +37,7 @@ def __init__(self, query, api, hostname_attr, environment=None): self.query = query self.api = api self.hostname_attr = hostname_attr - if isinstance(self.hostname_attr, basestring): + if isinstance(self.hostname_attr, six.string_types): self.hostname_attr = (self.hostname_attr,) self.environment = environment @@ -191,7 +190,7 @@ def migrate(): .. versionadded:: 0.2.1 """ # Allow passing a single iterable - if len(tags) == 1 and not isinstance(tags[0], basestring): + if len(tags) == 1 and not isinstance(tags[0], six.string_types): tags = tags[0] query = ' AND '.join('tags:%s'%tag.strip() for tag in tags) return chef_query(query, **kwargs) diff --git a/chef/node.py b/chef/node.py index 619d4bf..ea7666b 100644 --- a/chef/node.py +++ b/chef/node.py @@ -1,3 +1,4 @@ +import six import collections from chef.base import ChefObject @@ -27,7 +28,7 @@ def __init__(self, search_path=[], path=None, write=None): def __iter__(self): keys = set() for d in self.search_path: - keys |= set(d.iterkeys()) + keys |= set(six.iterkeys(d)) return iter(keys) def __len__(self): diff --git a/chef/rsa.py b/chef/rsa.py index a92c1cc..32ad3c0 100644 --- a/chef/rsa.py +++ b/chef/rsa.py @@ -1,3 +1,4 @@ +import six import sys from ctypes import * @@ -141,23 +142,22 @@ def __init__(self, fp=None): self.public = False if not fp: return - if isinstance(fp, basestring): - if fp.startswith('-----'): - # PEM formatted text - self.raw = fp - else: - self.raw = open(fp, 'rb').read() + if isinstance(fp, six.binary_type) and fp.startswith(b'-----'): + # PEM formatted text + self.raw = fp + elif isinstance(fp, six.string_types): + self.raw = open(fp, 'rb').read() else: self.raw = fp.read() self._load_key() def _load_key(self): - if '\0' in self.raw: + if b'\0' in self.raw: # Raw string has embedded nulls, treat it as binary data buf = create_string_buffer(self.raw, len(self.raw)) else: buf = create_string_buffer(self.raw) - + bio = BIO_new_mem_buf(buf, len(buf)) try: self.key = PEM_read_bio_RSAPrivateKey(bio, 0, 0, 0) @@ -179,7 +179,10 @@ def generate(cls, size=1024, exp=RSA_F4): def private_encrypt(self, value, padding=RSA_PKCS1_PADDING): if self.public: raise SSLError('private method cannot be used on a public key') - buf = create_string_buffer(value, len(value)) + if six.PY3 and not isinstance(value, bytes): + buf = create_string_buffer(value.encode(), len(value)) + else: + buf = create_string_buffer(value, len(value)) size = RSA_size(self.key) output = create_string_buffer(size) ret = RSA_private_encrypt(len(buf), buf, output, self.key, padding) @@ -188,13 +191,19 @@ def private_encrypt(self, value, padding=RSA_PKCS1_PADDING): return output.raw[:ret] def public_decrypt(self, value, padding=RSA_PKCS1_PADDING): - buf = create_string_buffer(value, len(value)) + if six.PY3 and not isinstance(value, bytes): + buf = create_string_buffer(value.encode(), len(value)) + else: + buf = create_string_buffer(value, len(value)) size = RSA_size(self.key) output = create_string_buffer(size) ret = RSA_public_decrypt(len(buf), buf, output, self.key, padding) if ret <= 0: raise SSLError('Unable to decrypt data') - return output.raw[:ret] + if six.PY3 and isinstance(output.raw, bytes): + return output.raw[:ret].decode() + else: + return output.raw[:ret] def private_export(self): if self.public: diff --git a/chef/search.py b/chef/search.py index 1fa96a1..ee1d940 100644 --- a/chef/search.py +++ b/chef/search.py @@ -1,6 +1,7 @@ +import six import collections import copy -import urllib +import six.moves.urllib.parse from chef.api import ChefAPI from chef.base import ChefQuery, ChefObject @@ -51,7 +52,7 @@ def __init__(self, index, q='*:*', rows=1000, start=0, api=None): self.name = index self.api = api or ChefAPI.get_global() self._args = dict(q=q, rows=rows, start=start) - self.url = self.__class__.url + '/' + self.name + '?' + urllib.urlencode(self._args) + self.url = self.__class__.url + '/' + self.name + '?' + six.moves.urllib.parse.urlencode(self._args) @property def data(self): @@ -86,7 +87,7 @@ def __getitem__(self, value): if value.step is not None and value.step != 1: raise ValueError('Cannot use a step other than 1') return self.start(self._args['start']+value.start).rows(value.stop-value.start) - if isinstance(value, basestring): + if isinstance(value, six.string_types): return self[self.index(value)] row_value = self.data['rows'][value] # Check for null rows, just in case @@ -112,5 +113,5 @@ def __call__(self, query): @classmethod def list(cls, api=None): api = api or ChefAPI.get_global() - names = [name for name, url in api[cls.url].iteritems()] + names = [name for name, url in six.iteritems(api[cls.url])] return ChefQuery(cls, names, api) diff --git a/chef/tests/__init__.py b/chef/tests/__init__.py index 71a3097..f5fddab 100644 --- a/chef/tests/__init__.py +++ b/chef/tests/__init__.py @@ -1,3 +1,4 @@ +import six.moves import os import random from functools import wraps @@ -50,12 +51,12 @@ def tearDown(self): for obj in self.objects: try: obj.delete() - except ChefError, e: - print e + except ChefError as e: + print(e) # Continue running def register(self, obj): self.objects.append(obj) def random(self, length=8, alphabet='0123456789abcdef'): - return ''.join(random.choice(alphabet) for _ in xrange(length)) + return ''.join(random.choice(alphabet) for _ in six.moves.range(length)) diff --git a/chef/tests/test_data_bag.py b/chef/tests/test_data_bag.py index 1a50d29..6d46bd3 100644 --- a/chef/tests/test_data_bag.py +++ b/chef/tests/test_data_bag.py @@ -10,7 +10,7 @@ def test_list(self): def test_keys(self): bag = DataBag('test_1') - self.assertItemsEqual(bag.keys(), ['item_1', 'item_2']) + self.assertItemsEqual(list(bag.keys()), ['item_1', 'item_2']) self.assertItemsEqual(iter(bag), ['item_1', 'item_2']) def test_item(self): diff --git a/chef/tests/test_fabric.py b/chef/tests/test_fabric.py index 4508c6c..88a5ae4 100644 --- a/chef/tests/test_fabric.py +++ b/chef/tests/test_fabric.py @@ -18,9 +18,9 @@ def search_mock(index, q='*:*', *args, **kwargs): search_mock_inst.data = data return search_mock_inst MockSearch.side_effect = search_mock - print MockSearch('role').data + print(MockSearch('role').data) @mockSearch({('role', '*:*'): {1:2}}) def test_roledef2(self, MockSearch): - print MockSearch('role').data + print(MockSearch('role').data) diff --git a/setup.py b/setup.py index 91534db..9a774f2 100644 --- a/setup.py +++ b/setup.py @@ -29,6 +29,7 @@ 'Programming Language :: Python', ], zip_safe = False, + install_requires = ['six>=1.9.0'], tests_require = ['unittest2', 'mock'], test_suite = 'unittest2.collector', )