diff --git a/.gitignore b/.gitignore index a8eee82..88c630b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,9 +4,8 @@ local_settings.py keeper build/* build-stamp -melange.egg-info -.melange-venv -.venv +python_melangeclient.egg-info +.tox *.sqlite *.log tags diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..df9dd8d --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include README.rst +include melange/client/views/*.tpl diff --git a/README b/README deleted file mode 100644 index 0cb0cfe..0000000 --- a/README +++ /dev/null @@ -1,11 +0,0 @@ - -To run unit tests: - melange_client_dir> ./run_tests.sh melange_client.tests.unit - -To run functional tests: -1. Start the melange server -2. Update the configuration values in - melange_client/tests/functional/tests.conf - to point to the melange server -3. Run the tests: - melange_client_dir> ./run_tests.sh melange_client.tests.functional diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..b5ea270 --- /dev/null +++ b/README.rst @@ -0,0 +1,16 @@ +Python bindings to the OpenStack Melange API +============================================ + +This is a client for the Openstack Melange API. It contains a Python API +(the ``melange.client`` module), and a command-line script (``melange``). + +Running the tests +----------------- + +Currently the test suite requires a running melange-server running on +http://localhost:9898. + +Tests are run under `tox `_. First install +``tox`` using pip or your distribution's packages then run ``tox`` from +the distribution directory to run the tests in isolated virtual +environments. diff --git a/melange/__init__.py b/melange/__init__.py new file mode 100644 index 0000000..7d0d297 --- /dev/null +++ b/melange/__init__.py @@ -0,0 +1,20 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import pkgutil + + +__path__ = pkgutil.extend_path(__path__, __name__) diff --git a/melange_client/__init__.py b/melange/client/__init__.py similarity index 71% rename from melange_client/__init__.py rename to melange/client/__init__.py index f077c6a..9ab7051 100644 --- a/melange_client/__init__.py +++ b/melange/client/__init__.py @@ -16,18 +16,15 @@ # under the License. import gettext -import os +from melange.client.client import HTTPClient +from melange.client.client import AuthorizationClient + + +# NOTE(jkoelker) should this be melange.client? Are translations going +# to be separate? gettext.install('melange', unicode=1) -def melange_root_path(): - return os.path.dirname(__file__) - - -def melange_bin_path(filename="."): - return os.path.join(melange_root_path(), "..", "bin", filename) - - -def melange_etc_path(filename="."): - return os.path.join(melange_root_path(), "..", "etc", "melange", filename) +__all__ = [HTTPClient, + AuthorizationClient] diff --git a/bin/melange b/melange/client/cli.py old mode 100755 new mode 100644 similarity index 87% rename from bin/melange rename to melange/client/cli.py index 653b8e0..8585b1a --- a/bin/melange +++ b/melange/client/cli.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2011 OpenStack LLC. @@ -29,22 +28,11 @@ from os import environ as env import sys import yaml -# If ../melange_client/__init__.py exists, add ../ to Python search path, so -# it will override what happens to be installed in /usr/(local/)lib/python... -possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), - os.pardir, - os.pardir)) -if os.path.exists(os.path.join(possible_topdir, - 'melange_client', - '__init__.py')): - sys.path.insert(0, possible_topdir) - -import melange_client -from melange_client import client as base_client -from melange_client import exception -from melange_client import inspector -from melange_client import ipam_client -from melange_client import template +from melange.client import client as base_client +from melange.client import exception +from melange.client import inspector +from melange.client import ipam_client +from melange.client import template def create_options(parser): @@ -164,7 +152,10 @@ def auth_client(options): def view(data, template_name): data = data or {} try: - view_path = os.path.join(melange_client.melange_root_path(), 'views') + # TODO(jkoelker) Templates should be using the PEP302 get_data api + melange_client_file = sys.modules['melange.client'].__file__ + melange_path = os.path.dirname(melange_client_file) + view_path = os.path.join(melange_path, 'views') return template.template(template_name, template_lookup=[view_path], **data) except exception.TemplateNotFoundError: @@ -179,17 +170,19 @@ def args_to_dict(args): "of the form of field=value") -def main(): +def main(script_name=None, argv=None): + if argv is None: + argv = sys.argv[1:] + + if script_name is None: + script_name = os.path.basename(sys.argv[0]) + oparser = optparse.OptionParser(version='%%prog 0.1', usage=usage()) create_options(oparser) - (options, args) = parse_options(oparser, sys.argv[1:]) + (options, args) = parse_options(oparser, argv) - script_name = os.path.basename(sys.argv[0]) category = args.pop(0) - http_client = base_client.HTTPClient(options.host, - options.port, - options.timeout) factory = ipam_client.Factory(options.host, options.port, @@ -246,7 +239,3 @@ def main(): else: print _("Command failed, please check log for more info") sys.exit(2) - - -if __name__ == '__main__': - main() diff --git a/melange_client/client.py b/melange/client/client.py similarity index 94% rename from melange_client/client.py rename to melange/client/client.py index d011422..da35c80 100644 --- a/melange_client/client.py +++ b/melange/client/client.py @@ -15,14 +15,15 @@ # License for the specific language governing permissions and limitations # under the License. + import httplib import httplib2 -import json import socket import urllib import urlparse -from melange_client import exception +from melange.client import exception +from melange.client import utils class HTTPClient(object): @@ -75,7 +76,7 @@ class AuthorizationClient(httplib2.Http): if self.auth_token: return self.auth_token headers = {'content-type': 'application/json'} - request_body = json.dumps({"passwordCredentials": + request_body = utils.dumps({"passwordCredentials": {"username": self.username, 'password': self.access_key}}) res, body = self.request(self.url, "POST", headers=headers, @@ -83,4 +84,4 @@ class AuthorizationClient(httplib2.Http): if int(res.status) >= 400: raise Exception(_("Error occured while retrieving token : %s") % body) - return json.loads(body)['auth']['token']['id'] + return utils.loads(body)['auth']['token']['id'] diff --git a/melange_client/exception.py b/melange/client/exception.py similarity index 90% rename from melange_client/exception.py rename to melange/client/exception.py index c16d303..564c451 100644 --- a/melange_client/exception.py +++ b/melange/client/exception.py @@ -16,10 +16,10 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack.common import exception as openstack_exception - -ClientConnectionError = openstack_exception.ClientConnectionError +class ClientConnectionError(Exception): + """Error resulting from a client connecting to a server""" + pass class MelangeClientError(Exception): diff --git a/melange_client/inspector.py b/melange/client/inspector.py similarity index 100% rename from melange_client/inspector.py rename to melange/client/inspector.py diff --git a/melange_client/ipam_client.py b/melange/client/ipam_client.py similarity index 98% rename from melange_client/ipam_client.py rename to melange/client/ipam_client.py index 0c6c0e9..9d75cfb 100644 --- a/melange_client/ipam_client.py +++ b/melange/client/ipam_client.py @@ -15,12 +15,11 @@ # License for the specific language governing permissions and limitations # under the License. -import json import sys import urlparse -from melange_client import client -from melange_client import utils +from melange.client import client +from melange.client import utils class Factory(object): @@ -71,12 +70,12 @@ class Resource(object): def create(self, **kwargs): return self.request("POST", self.path, - body=json.dumps({self.name: kwargs})) + body=utils.dumps({self.name: kwargs})) def update(self, id, **kwargs): return self.request("PUT", self._member_path(id), - body=json.dumps( + body=utils.dumps( {self.name: utils.remove_nones(kwargs)})) def all(self, **params): @@ -99,7 +98,7 @@ class Resource(object): kwargs['headers']['X-AUTH-TOKEN'] = self.auth_client.get_token() result = self.client.do_request(method, path, **kwargs).read() if result: - return json.loads(result) + return utils.loads(result) class BaseClient(object): diff --git a/melange_client/template.py b/melange/client/template.py similarity index 99% rename from melange_client/template.py rename to melange/client/template.py index 19dcc63..29f0c68 100644 --- a/melange_client/template.py +++ b/melange/client/template.py @@ -41,12 +41,9 @@ import cgi import re import os import functools -import time import tokenize -import mimetypes -import datetime -from melange_client import exception +from melange.client import exception TEMPLATES = {} DEBUG = False diff --git a/melange_client/tests/__init__.py b/melange/client/tests/__init__.py similarity index 97% rename from melange_client/tests/__init__.py rename to melange/client/tests/__init__.py index a8c6bda..2889dd8 100644 --- a/melange_client/tests/__init__.py +++ b/melange/client/tests/__init__.py @@ -18,6 +18,7 @@ import unittest +# TODO(jkoelker) Convert this to mock import mox diff --git a/melange/client/tests/functional/__init__.py b/melange/client/tests/functional/__init__.py new file mode 100644 index 0000000..b601379 --- /dev/null +++ b/melange/client/tests/functional/__init__.py @@ -0,0 +1,59 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import ConfigParser +import cStringIO +import os +import sys + +from melange.client import cli + + +def run(command, **kwargs): + config = ConfigParser.ConfigParser() + functional_path = os.path.dirname(os.path.realpath(__file__)) + config.read(os.path.join(functional_path, "tests.conf")) + + stdout = sys.stdout + stderr = sys.stderr + exit = sys.exit + + mystdout = cStringIO.StringIO() + mystderr = cStringIO.StringIO() + exitcode = {'code': 0} + + def myexit(code): + exitcode['code'] = code + + sys.stdout = mystdout + sys.stderr = mystderr + sys.exit = myexit + + argv = ['--host=%s' % config.get('DEFAULT', 'server_name'), + '--port=%s' % config.get('DEFAULT', 'server_port'), + '-v'] + argv.extend(command.strip().split(' ')) + + cli.main(script_name='melange', argv=argv) + + sys.stdout = stdout + sys.stderr = stderr + sys.exit = exit + + return {'exitcode': exitcode['code'], + 'out': mystdout.getvalue(), + 'err': mystderr.getvalue()} diff --git a/melange_client/tests/functional/factory.py b/melange/client/tests/functional/factory.py similarity index 98% rename from melange_client/tests/functional/factory.py rename to melange/client/tests/functional/factory.py index 9b2103f..d4a8dd0 100644 --- a/melange_client/tests/functional/factory.py +++ b/melange/client/tests/functional/factory.py @@ -18,7 +18,7 @@ import uuid import yaml -from melange_client.tests import functional +from melange.client.tests import functional def create_policy(tenant_id="123"): diff --git a/melange_client/tests/functional/template_test_helper.py b/melange/client/tests/functional/template_test_helper.py similarity index 100% rename from melange_client/tests/functional/template_test_helper.py rename to melange/client/tests/functional/template_test_helper.py diff --git a/melange_client/tests/functional/test_cli.py b/melange/client/tests/functional/test_cli.py similarity index 98% rename from melange_client/tests/functional/test_cli.py rename to melange/client/tests/functional/test_cli.py index 592bd78..59cc9c0 100644 --- a/melange_client/tests/functional/test_cli.py +++ b/melange/client/tests/functional/test_cli.py @@ -15,15 +15,14 @@ # License for the specific language governing permissions and limitations # under the License. -import re import uuid import yaml -from melange_client import tests -from melange_client import utils -from melange_client.tests import functional -from melange_client.tests.functional import template_test_helper -from melange_client.tests.functional import factory +from melange.client import tests +from melange.client import utils +from melange.client.tests import functional +from melange.client.tests.functional import template_test_helper +from melange.client.tests.functional import factory class TestBaseCLI(tests.BaseTest, template_test_helper.TemplateTestHelper): @@ -109,9 +108,8 @@ class TestIpBlockCLI(TestBaseCLI): sorted(factory.model('ip_blocks', list_res))) def test_list_without_tenant_id_should_error_out(self): - self.assertRaises(RuntimeError, - functional.run, - "ip_block list") + err_res = functional.run("ip_block list") + self.assertTrue(0 != err_res['exitcode']) class TestSubnetCLI(TestBaseCLI): diff --git a/melange_client/tests/functional/tests.conf b/melange/client/tests/functional/tests.conf similarity index 100% rename from melange_client/tests/functional/tests.conf rename to melange/client/tests/functional/tests.conf diff --git a/melange_client/tests/unit/__init__.py b/melange/client/tests/unit/__init__.py similarity index 100% rename from melange_client/tests/unit/__init__.py rename to melange/client/tests/unit/__init__.py diff --git a/melange_client/tests/unit/helper/__init__.py b/melange/client/tests/unit/helper/__init__.py similarity index 100% rename from melange_client/tests/unit/helper/__init__.py rename to melange/client/tests/unit/helper/__init__.py diff --git a/melange_client/tests/unit/helper/test_table.py b/melange/client/tests/unit/helper/test_table.py similarity index 94% rename from melange_client/tests/unit/helper/test_table.py rename to melange/client/tests/unit/helper/test_table.py index c6c5d60..4fb6d76 100644 --- a/melange_client/tests/unit/helper/test_table.py +++ b/melange/client/tests/unit/helper/test_table.py @@ -16,8 +16,8 @@ # License for the specific language governing permissions and limitations # under the License. -from melange_client import tests -from melange_client.views.helpers import table +from melange.client import tests +from melange.client.views.helpers import table class TestTable(tests.BaseTest): diff --git a/melange_client/tests/unit/test_client.py b/melange/client/tests/unit/test_client.py similarity index 94% rename from melange_client/tests/unit/test_client.py rename to melange/client/tests/unit/test_client.py index 4747479..59800ec 100644 --- a/melange_client/tests/unit/test_client.py +++ b/melange/client/tests/unit/test_client.py @@ -16,14 +16,20 @@ # License for the specific language governing permissions and limitations # under the License. -import json +try: + import simplejson + json = simplejson +except ImportError: + import json + import urlparse import httplib2 +# TODO(jkoelker) Convert this to mock import mox -from melange_client import client -from melange_client import tests +from melange.client import client +from melange.client import tests class TestAuthorizationClient(tests.BaseTest): diff --git a/melange_client/tests/unit/test_inspector.py b/melange/client/tests/unit/test_inspector.py similarity index 97% rename from melange_client/tests/unit/test_inspector.py rename to melange/client/tests/unit/test_inspector.py index 42ec675..973c9e9 100644 --- a/melange_client/tests/unit/test_inspector.py +++ b/melange/client/tests/unit/test_inspector.py @@ -16,8 +16,8 @@ # License for the specific language governing permissions and limitations # under the License. -from melange_client import tests -from melange_client import inspector +from melange.client import tests +from melange.client import inspector class TestMethodInspector(tests.BaseTest): diff --git a/melange_client/tests/unit/test_ipam_client.py b/melange/client/tests/unit/test_ipam_client.py similarity index 94% rename from melange_client/tests/unit/test_ipam_client.py rename to melange/client/tests/unit/test_ipam_client.py index 09920cf..3e1f1e0 100644 --- a/melange_client/tests/unit/test_ipam_client.py +++ b/melange/client/tests/unit/test_ipam_client.py @@ -16,8 +16,8 @@ # License for the specific language governing permissions and limitations # under the License. -from melange_client import ipam_client -from melange_client import tests +from melange.client import ipam_client +from melange.client import tests class TestFactory(tests.BaseTest): diff --git a/melange_client/tests/unit/test_utils.py b/melange/client/tests/unit/test_utils.py similarity index 95% rename from melange_client/tests/unit/test_utils.py rename to melange/client/tests/unit/test_utils.py index af3292c..610fe98 100644 --- a/melange_client/tests/unit/test_utils.py +++ b/melange/client/tests/unit/test_utils.py @@ -15,8 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. -from melange_client import tests -from melange_client import utils +from melange.client import tests +from melange.client import utils class TestUtils(tests.BaseTest): diff --git a/melange_client/tests/functional/__init__.py b/melange/client/utils.py similarity index 53% rename from melange_client/tests/functional/__init__.py rename to melange/client/utils.py index 20ececc..9864b0a 100644 --- a/melange_client/tests/functional/__init__.py +++ b/melange/client/utils.py @@ -15,20 +15,29 @@ # License for the specific language governing permissions and limitations # under the License. -import ConfigParser -import os +import re -import melange_client -from melange_client import utils +try: + # For Python < 2.6 or people using a newer version of simplejson + import simplejson + json = simplejson +except ImportError: + # For Python >= 2.6 + import json -def run(command, **kwargs): - config = ConfigParser.ConfigParser() - config.read(os.path.join(melange_client.melange_root_path(), - "tests/functional/tests.conf")) - full_command = "{0} --host={1} --port={2} {3} -v ".format( - melange_client.melange_bin_path('melange'), - config.get('DEFAULT', 'server_name'), - config.get('DEFAULT', 'server_port'), - command) - return utils.execute(full_command, **kwargs) +def loads(s): + return json.loads(s) + + +def dumps(o): + return json.dumps(o) + + +def camelize(string): + return re.sub(r"(?:^|_)(.)", lambda x: x.group(0)[-1].upper(), string) + + +def remove_nones(hash): + return dict((key, value) + for key, value in hash.iteritems() if value is not None) diff --git a/melange_client/views/__init__.py b/melange/client/views/__init__.py similarity index 100% rename from melange_client/views/__init__.py rename to melange/client/views/__init__.py diff --git a/melange_client/views/helpers/__init__.py b/melange/client/views/helpers/__init__.py similarity index 100% rename from melange_client/views/helpers/__init__.py rename to melange/client/views/helpers/__init__.py diff --git a/melange_client/views/helpers/table.py b/melange/client/views/helpers/table.py similarity index 100% rename from melange_client/views/helpers/table.py rename to melange/client/views/helpers/table.py diff --git a/melange_client/views/ip_route_list.tpl b/melange/client/views/ip_route_list.tpl similarity index 80% rename from melange_client/views/ip_route_list.tpl rename to melange/client/views/ip_route_list.tpl index 27ebbf1..80584bf 100644 --- a/melange_client/views/ip_route_list.tpl +++ b/melange/client/views/ip_route_list.tpl @@ -1,4 +1,4 @@ -%from melange_client.views.helpers import table +%from melange.client.views.helpers import table {{table.row_view(table.padded_keys(ip_routes).iteritems())}} %for route in ip_routes: {{table.row_view(map(lambda (key,value): (route[key], value), table.padded_keys(ip_routes).iteritems()))}} diff --git a/melange_client/utils.py b/melange_client/utils.py deleted file mode 100644 index 56153b6..0000000 --- a/melange_client/utils.py +++ /dev/null @@ -1,65 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import re -import os -import subprocess - -import melange_client - - -def camelize(string): - return re.sub(r"(?:^|_)(.)", lambda x: x.group(0)[-1].upper(), string) - - -def remove_nones(hash): - return dict((key, value) - for key, value in hash.iteritems() if value is not None) - - -def execute(cmd, raise_error=True): - """Executes a command in a subprocess. - Returns a tuple of (exitcode, out, err), where out is the string output - from stdout and err is the string output from stderr when - executing the command. - - :param cmd: Command string to execute - :param raise_error: If returncode is not 0 (success), then - raise a RuntimeError? Default: True) - - """ - - env = os.environ.copy() - - # Make sure that we use the programs in the - # current source directory's bin/ directory. - env['PATH'] = melange_client.melange_bin_path() + ':' + env['PATH'] - process = subprocess.Popen(cmd, - shell=True, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=env) - (out, err) = process.communicate() - exitcode = process.returncode - if process.returncode != 0 and raise_error: - msg = "Command %(cmd)s did not succeed. Returned an exit "\ - "code of %(exitcode)d."\ - "\n\nSTDOUT: %(out)s"\ - "\n\nSTDERR: %(err)s" % locals() - raise RuntimeError(msg) - return {'exitcode': exitcode, 'out': out, 'err': err} diff --git a/run_tests.py b/run_tests.py deleted file mode 100644 index f90c44e..0000000 --- a/run_tests.py +++ /dev/null @@ -1,367 +0,0 @@ -#!/usr/bin/env python -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Colorizer Code is borrowed from Twisted: -# Copyright (c) 2001-2010 Twisted Matrix Laboratories. -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 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. -"""Unittest runner for Nova. - -To run all tests - python run_tests.py - -To run a single test: - python run_tests.py test_compute:ComputeTestCase.test_run_terminate - -To run a single test module: - python run_tests.py test_compute - - or - - python run_tests.py api.test_wsgi - -""" - -import gettext -import heapq -import logging -import os -import unittest -import sys -import time - -gettext.install('melange', unicode=1) - -from nose import config -from nose import core -from nose import result - - -class _AnsiColorizer(object): - """ - A colorizer is an object that loosely wraps around a stream, allowing - callers to write text to the stream in a particular color. - - Colorizer classes must implement C{supported()} and C{write(text, color)}. - """ - _colors = dict(black=30, red=31, green=32, yellow=33, - blue=34, magenta=35, cyan=36, white=37) - - def __init__(self, stream): - self.stream = stream - - def supported(cls, stream=sys.stdout): - """ - A class method that returns True if the current platform supports - coloring terminal output using this method. Returns False otherwise. - """ - if not stream.isatty(): - return False # auto color only on TTYs - try: - import curses - except ImportError: - return False - else: - try: - try: - return curses.tigetnum("colors") > 2 - except curses.error: - curses.setupterm() - return curses.tigetnum("colors") > 2 - except: - raise - # guess false in case of error - return False - supported = classmethod(supported) - - def write(self, text, color): - """ - Write the given text to the stream in the given color. - - @param text: Text to be written to the stream. - - @param color: A string label for a color. e.g. 'red', 'white'. - """ - color = self._colors[color] - self.stream.write('\x1b[%s;1m%s\x1b[0m' % (color, text)) - - -class _Win32Colorizer(object): - """ - See _AnsiColorizer docstring. - """ - def __init__(self, stream): - from win32console import GetStdHandle, STD_OUT_HANDLE, \ - FOREGROUND_RED, FOREGROUND_BLUE, FOREGROUND_GREEN, \ - FOREGROUND_INTENSITY - red, green, blue, bold = (FOREGROUND_RED, FOREGROUND_GREEN, - FOREGROUND_BLUE, FOREGROUND_INTENSITY) - self.stream = stream - self.screenBuffer = GetStdHandle(STD_OUT_HANDLE) - self._colors = { - 'normal': red | green | blue, - 'red': red | bold, - 'green': green | bold, - 'blue': blue | bold, - 'yellow': red | green | bold, - 'magenta': red | blue | bold, - 'cyan': green | blue | bold, - 'white': red | green | blue | bold - } - - def supported(cls, stream=sys.stdout): - try: - import win32console - screenBuffer = win32console.GetStdHandle( - win32console.STD_OUT_HANDLE) - except ImportError: - return False - import pywintypes - try: - screenBuffer.SetConsoleTextAttribute( - win32console.FOREGROUND_RED | - win32console.FOREGROUND_GREEN | - win32console.FOREGROUND_BLUE) - except pywintypes.error: - return False - else: - return True - supported = classmethod(supported) - - def write(self, text, color): - color = self._colors[color] - self.screenBuffer.SetConsoleTextAttribute(color) - self.stream.write(text) - self.screenBuffer.SetConsoleTextAttribute(self._colors['normal']) - - -class _NullColorizer(object): - """ - See _AnsiColorizer docstring. - """ - def __init__(self, stream): - self.stream = stream - - def supported(cls, stream=sys.stdout): - return True - supported = classmethod(supported) - - def write(self, text, color): - self.stream.write(text) - - -def get_elapsed_time_color(elapsed_time): - if elapsed_time > 1.0: - return 'red' - elif elapsed_time > 0.25: - return 'yellow' - else: - return 'green' - - -class MelangeTestResult(result.TextTestResult): - def __init__(self, *args, **kw): - self.show_elapsed = kw.pop('show_elapsed') - result.TextTestResult.__init__(self, *args, **kw) - self.num_slow_tests = 5 - self.slow_tests = [] # this is a fixed-sized heap - self._last_case = None - self.colorizer = None - # NOTE(vish): reset stdout for the terminal check - stdout = sys.stdout - sys.stdout = sys.__stdout__ - for colorizer in [_Win32Colorizer, _AnsiColorizer, _NullColorizer]: - if colorizer.supported(): - self.colorizer = colorizer(self.stream) - break - sys.stdout = stdout - - # NOTE(lorinh): Initialize start_time in case a sqlalchemy-migrate - # error results in it failing to be initialized later. Otherwise, - # _handleElapsedTime will fail, causing the wrong error message to - # be outputted. - self.start_time = time.time() - - def getDescription(self, test): - return str(test) - - def _handleElapsedTime(self, test): - self.elapsed_time = time.time() - self.start_time - item = (self.elapsed_time, test) - # Record only the n-slowest tests using heap - if len(self.slow_tests) >= self.num_slow_tests: - heapq.heappushpop(self.slow_tests, item) - else: - heapq.heappush(self.slow_tests, item) - - def _writeElapsedTime(self, test): - color = get_elapsed_time_color(self.elapsed_time) - self.colorizer.write(" %.2f" % self.elapsed_time, color) - - def _writeResult(self, test, long_result, color, short_result, success): - if self.showAll: - self.colorizer.write(long_result, color) - if self.show_elapsed and success: - self._writeElapsedTime(test) - self.stream.writeln() - elif self.dots: - self.stream.write(short_result) - self.stream.flush() - - # NOTE(vish): copied from unittest with edit to add color - def addSuccess(self, test): - unittest.TestResult.addSuccess(self, test) - self._handleElapsedTime(test) - self._writeResult(test, 'OK', 'green', '.', True) - - # NOTE(vish): copied from unittest with edit to add color - def addFailure(self, test, err): - unittest.TestResult.addFailure(self, test, err) - self._handleElapsedTime(test) - self._writeResult(test, 'FAIL', 'red', 'F', False) - - # NOTE(vish): copied from nose with edit to add color - def addError(self, test, err): - """Overrides normal addError to add support for - errorClasses. If the exception is a registered class, the - error will be added to the list for that class, not errors. - """ - self._handleElapsedTime(test) - stream = getattr(self, 'stream', None) - ec, ev, tb = err - try: - exc_info = self._exc_info_to_string(err, test) - except TypeError: - # 2.3 compat - exc_info = self._exc_info_to_string(err) - for cls, (storage, label, isfail) in self.errorClasses.items(): - if result.isclass(ec) and issubclass(ec, cls): - if isfail: - test.passed = False - storage.append((test, exc_info)) - # Might get patched into a streamless result - if stream is not None: - if self.showAll: - message = [label] - detail = result._exception_detail(err[1]) - if detail: - message.append(detail) - stream.writeln(": ".join(message)) - elif self.dots: - stream.write(label[:1]) - return - self.errors.append((test, exc_info)) - test.passed = False - if stream is not None: - self._writeResult(test, 'ERROR', 'red', 'E', False) - - def startTest(self, test): - unittest.TestResult.startTest(self, test) - self.start_time = time.time() - current_case = test.test.__class__.__name__ - - if self.showAll: - if current_case != self._last_case: - self.stream.writeln(current_case) - self._last_case = current_case - - self.stream.write( - ' %s' % str(test.test._testMethodName).ljust(60)) - self.stream.flush() - - -class MelangeTestRunner(core.TextTestRunner): - def __init__(self, *args, **kwargs): - self.show_elapsed = kwargs.pop('show_elapsed') - core.TextTestRunner.__init__(self, *args, **kwargs) - - def _makeResult(self): - return MelangeTestResult(self.stream, - self.descriptions, - self.verbosity, - self.config, - show_elapsed=self.show_elapsed) - - def _writeSlowTests(self, result_): - # Pare out 'fast' tests - slow_tests = [item for item in result_.slow_tests - if get_elapsed_time_color(item[0]) != 'green'] - if slow_tests: - slow_total_time = sum(item[0] for item in slow_tests) - self.stream.writeln("Slowest %i tests took %.2f secs:" - % (len(slow_tests), slow_total_time)) - for elapsed_time, test in sorted(slow_tests, reverse=True): - time_str = "%.2f" % elapsed_time - self.stream.writeln(" %s %s" % (time_str.ljust(10), test)) - - def run(self, test): - result_ = core.TextTestRunner.run(self, test) - if self.show_elapsed: - self._writeSlowTests(result_) - return result_ - - -if __name__ == '__main__': - logger = logging.getLogger() - hdlr = logging.StreamHandler() - formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') - hdlr.setFormatter(formatter) - logger.addHandler(hdlr) - logger.setLevel(logging.DEBUG) - # If any argument looks like a test name but doesn't have "melange.tests" in - # front of it, automatically add that so we don't have to type as much - show_elapsed = True - argv = [] - for x in sys.argv: - if x.startswith('test_'): - argv.append('melange.tests.%s' % x) - elif x.startswith('--hide-elapsed'): - show_elapsed = False - else: - argv.append(x) - - testdir = os.path.abspath(os.path.join("melange_client", "tests")) - c = config.Config(stream=sys.stdout, - env=os.environ, - verbosity=3, - workingDir=testdir, - plugins=core.DefaultPluginManager()) - - runner = MelangeTestRunner(stream=c.stream, - verbosity=c.verbosity, - config=c, - show_elapsed=show_elapsed) - sys.exit(not core.run(config=c, testRunner=runner, argv=argv)) diff --git a/run_tests.sh b/run_tests.sh deleted file mode 100755 index 26a5d18..0000000 --- a/run_tests.sh +++ /dev/null @@ -1,168 +0,0 @@ -#!/bin/bash - -set -eu - -function usage { - echo "Usage: $0 [OPTION]..." - echo "Run Melange's test suite(s)" - echo "" - echo " -V, --virtual-env Always use virtualenv. Install automatically if not present" - echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment" - echo " -r, --recreate-db Recreate the test database (deprecated, as this is now the default)." - echo " -n, --no-recreate-db Don't recreate the test database." - echo " -x, --stop Stop running tests after the first error or failure." - echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added." - echo " -p, --pep8 Just run pep8" - echo " -P, --no-pep8 Don't run pep8" - echo " -c, --coverage Generate coverage report" - echo " -h, --help Print this usage message" - echo " --hide-elapsed Don't print the elapsed time for each test along with slow test list" - echo "" - echo "Note: with no options specified, the script will try to run the tests in a virtual environment," - echo " If no virtualenv is found, the script will ask if you would like to create one. If you " - echo " prefer to run tests NOT in a virtual environment, simply pass the -N option." - exit -} - -function process_option { - case "$1" in - -h|--help) usage;; - -V|--virtual-env) always_venv=1; never_venv=0;; - -N|--no-virtual-env) always_venv=0; never_venv=1;; - -r|--recreate-db) recreate_db=1;; - -n|--no-recreate-db) recreate_db=0;; - -f|--force) force=1;; - -p|--pep8) just_pep8=1;; - -P|--no-pep8) no_pep8=1;; - -c|--coverage) coverage=1;; - -*) noseopts="$noseopts $1";; - *) noseargs="$noseargs $1" - esac -} - -venv=.venv -with_venv=tools/with_venv.sh -always_venv=0 -never_venv=0 -force=0 -noseargs= -noseopts= -wrapper="" -just_pep8=0 -no_pep8=0 -coverage=0 -recreate_db=1 - -for arg in "$@"; do - process_option $arg -done - -# If enabled, tell nose to collect coverage data -if [ $coverage -eq 1 ]; then - noseopts="$noseopts --with-coverage --cover-package=melange" -fi - -function run_tests { - # Just run the test suites in current environment - ${wrapper} $NOSETESTS 2> run_tests.log - # If we get some short import error right away, print the error log directly - RESULT=$? - if [ "$RESULT" -ne "0" ]; - then - ERRSIZE=`wc -l run_tests.log | awk '{print \$1}'` - if [ "$ERRSIZE" -lt "40" ]; - then - cat run_tests.log - fi - fi - return $RESULT -} - -function run_pep8 { - echo "Running pep8 ..." - # Opt-out files from pep8 - ignore_scripts="*.sh:*melange-debug:*clean-vlans" - ignore_files="*eventlet-patch:*pip-requires" - GLOBIGNORE="$ignore_scripts:$ignore_files" - srcfiles=`find bin -type f ! -name "melange.conf*"` - srcfiles+=" `find tools/*`" - srcfiles+=" bin melange_client" - # Just run PEP8 in current environment - # - # NOTE(sirp): W602 (deprecated 3-arg raise) is being ignored for the - # following reasons: - # - # 1. It's needed to preserve traceback information when re-raising - # exceptions; this is needed b/c Eventlet will clear exceptions when - # switching contexts. - # - # 2. There doesn't appear to be an alternative, "pep8-tool" compatible way of doing this - # in Python 2 (in Python 3 `with_traceback` could be used). - # - # 3. Can find no corroborating evidence that this is deprecated in Python 2 - # other than what the PEP8 tool claims. It is deprecated in Python 3, so, - # perhaps the mistake was thinking that the deprecation applied to Python 2 - # as well. - ${wrapper} pep8 --repeat --show-pep8 --show-source \ - --ignore=E202,W602 \ - --exclude=vcsversion.py ${srcfiles} -} - -NOSETESTS="python run_tests.py $noseopts $noseargs" - -if [ $never_venv -eq 0 ] -then - # Remove the virtual environment if --force used - if [ $force -eq 1 ]; then - echo "Cleaning virtualenv..." - rm -rf ${venv} - fi - if [ -e ${venv} ]; then - wrapper="${with_venv}" - else - if [ $always_venv -eq 1 ]; then - # Automatically install the virtualenv - python tools/install_venv.py - wrapper="${with_venv}" - else - echo -e "No virtual environment found...create one? (Y/n) \c" - read use_ve - if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then - # Install the virtualenv and run the test suite in it - python tools/install_venv.py - wrapper=${with_venv} - fi - fi - fi -fi - -# Delete old coverage data from previous runs -if [ $coverage -eq 1 ]; then - ${wrapper} coverage erase -fi - -if [ $just_pep8 -eq 1 ]; then - run_pep8 - exit -fi - -if [ $recreate_db -eq 1 ]; then - rm -f tests.sqlite -fi - -run_tests - -# NOTE(sirp): we only want to run pep8 when we're running the full-test suite, -# not when we're running tests individually. To handle this, we need to -# distinguish between options (noseopts), which begin with a '-', and -# arguments (noseargs). -if [ -z "$noseargs" ]; then - if [ $no_pep8 -eq 0 ]; then - run_pep8 - fi -fi - -if [ $coverage -eq 1 ]; then - echo "Generating coverage report in covhtml/" - ${wrapper} coverage html -d covhtml -i -fi diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..8bbf905 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,15 @@ +[nosetests] +verbosity=2 +detailed-errors=1 +with-tissue=1 +tissue-repeat=1 +tissue-show-pep8=1 +tissue-show-source=1 +tissue-inclusive=1 +tissue-color=1 +tissue-package=melange.client +with-openstack=1 +openstack-red=0.1 +openstack-yellow=0.075 +openstack-show-elapsed=1 +openstack-color=1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2b4ea14 --- /dev/null +++ b/setup.py @@ -0,0 +1,57 @@ +# Copyright 2011 OpenStack, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys + +import setuptools + +version = "0.1" +install_requires = ["httplib2", "pyyaml"] + +if sys.version_info < (2, 6): + install_requires.append("simplejson") + +classifiers = ["Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python", + ] + +console_scripts = ["melange = melange.client.cli:main"] + + +def read_file(file_name): + return open(os.path.join(os.path.dirname(__file__), + file_name)).read() + + +setuptools.setup(name="python-melangeclient", + version=version, + description="Client library for OpenStack Melange API.", + long_description=read_file("README.rst"), + license="Apache License, Version 2.0", + url="https://github.com/openstack/python-melangeclient", + classifiers=classifiers, + author="Openstack Melange Team", + author_email="openstack@lists.launchpad.net", + include_package_data=True, + packages=setuptools.find_packages(exclude=["tests"]), + install_requires=install_requires, + entry_points = {"console_scripts": console_scripts}, + zip_safe=False, +) diff --git a/tools/install_venv.py b/tools/install_venv.py deleted file mode 100644 index 952b21c..0000000 --- a/tools/install_venv.py +++ /dev/null @@ -1,145 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2010 OpenStack, LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Installation script for Melange client's development virtualenv. -""" - -import os -import subprocess -import sys - - -ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) -VENV = os.path.join(ROOT, '.venv') -PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires') -PY_VERSION = "python%s.%s" % (sys.version_info[0], sys.version_info[1]) - - -def die(message, *args): - print >> sys.stderr, message % args - sys.exit(1) - - -def check_python_version(): - if sys.version_info < (2, 6): - die("Need Python Version >= 2.6") - - -def run_command(cmd, redirect_output=True, check_exit_code=True): - """ - Runs a command in an out-of-process shell, returning the - output of that command. Working directory is ROOT. - """ - if redirect_output: - stdout = subprocess.PIPE - else: - stdout = None - - proc = subprocess.Popen(cmd, cwd=ROOT, stdout=stdout) - output = proc.communicate()[0] - if check_exit_code and proc.returncode != 0: - die('Command "%s" failed.\n%s', ' '.join(cmd), output) - return output - - -HAS_EASY_INSTALL = bool(run_command(['which', 'easy_install'], - check_exit_code=False).strip()) -HAS_VIRTUALENV = bool(run_command(['which', 'virtualenv'], - check_exit_code=False).strip()) - - -def check_dependencies(): - """Make sure virtualenv is in the path.""" - - if not HAS_VIRTUALENV: - print 'not found.' - # Try installing it via easy_install... - if HAS_EASY_INSTALL: - print 'Installing virtualenv via easy_install...', - if not (run_command(['which', 'easy_install']) and - run_command(['easy_install', 'virtualenv'])): - die('ERROR: virtualenv not found.\n\nMelange client' - ' development requires virtualenv, please install it' - ' using your favorite package management tool') - print 'done.' - print 'done.' - - -def create_virtualenv(venv=VENV): - """Creates the virtual environment and installs PIP only into the - virtual environment - """ - print 'Creating venv...', - run_command(['virtualenv', '-q', '--no-site-packages', VENV]) - print 'done.' - print 'Installing pip in virtualenv...', - if not run_command(['tools/with_venv.sh', 'easy_install', 'pip']).strip(): - die("Failed to install pip.") - print 'done.' - - -def install_dependencies(venv=VENV): - print 'Installing dependencies with pip (this can take a while)...' - # Install greenlet by hand - just listing it in the requires file does not - # get it in stalled in the right order - run_command(['tools/with_venv.sh', 'pip', 'install', '-E', venv, - 'greenlet'], redirect_output=False) - run_command(['tools/with_venv.sh', 'pip', 'install', '-E', venv, '-r', - PIP_REQUIRES], redirect_output=False) - - # Tell the virtual env how to "import melange" - pthfile = os.path.join(venv, "lib", PY_VERSION, "site-packages", - "melange.pth") - f = open(pthfile, 'w') - f.write("%s\n" % ROOT) - - -def print_help(): - help = """ - Melange client development environment setup is complete. - - Melange client development uses virtualenv to track and manage Python - dependencies while in development and testing. - - To activate the Melange client virtualenv for the extent of your current - shell session you can run: - - $ source .venv/bin/activate - - Or, if you prefer, you can run commands in the virtualenv on a case by case - basis by running: - - $ tools/with_venv.sh - - Also, make test will automatically use the virtualenv. - """ - print help - - -def main(argv): - check_python_version() - check_dependencies() - create_virtualenv() - install_dependencies() - print_help() - -if __name__ == '__main__': - main(sys.argv) diff --git a/tools/pip-requires b/tools/pip-requires deleted file mode 100644 index a2abbe2..0000000 --- a/tools/pip-requires +++ /dev/null @@ -1,10 +0,0 @@ -pep8 -pylint -mox -nose -sphinx -coverage -nosexcover -httplib2 -pyyaml --e git+https://github.com/jkoelker/openstack-common.git@melange_compat#egg=openstack.common diff --git a/tools/with_venv.sh b/tools/with_venv.sh deleted file mode 100755 index c8d2940..0000000 --- a/tools/with_venv.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -TOOLS=`dirname $0` -VENV=$TOOLS/../.venv -source $VENV/bin/activate && $@ diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..579d32c --- /dev/null +++ b/tox.ini @@ -0,0 +1,9 @@ +[tox] +envlist = py26,py27 + +[testenv] +deps= nose + mox + tissue + openstack.nose_plugin +commands=nosetests []