Merge lp:~tvansteenburgh/juju-deployer/python3-support into lp:juju-deployer

Proposed by Tim Van Steenburgh
Status: Merged
Merged at revision: 174
Proposed branch: lp:~tvansteenburgh/juju-deployer/python3-support
Merge into: lp:juju-deployer
Diff against target: 3879 lines (+261/-2524)
43 files modified
.bzrignore (+1/-0)
HACKING (+3/-2)
Makefile (+1/-9)
deployer/action/diff.py (+8/-5)
deployer/action/export.py (+1/-0)
deployer/action/importer.py (+4/-1)
deployer/charm.py (+4/-2)
deployer/cli.py (+5/-3)
deployer/config.py (+10/-8)
deployer/deployment.py (+20/-17)
deployer/env/__init__.py (+0/-3)
deployer/env/base.py (+2/-1)
deployer/env/go.py (+5/-4)
deployer/env/mem.py (+5/-3)
deployer/env/py.py (+1/-3)
deployer/env/watchers.py (+2/-1)
deployer/errors.py (+4/-2)
deployer/feedback.py (+5/-0)
deployer/guiserver.py (+1/-0)
deployer/relation.py (+3/-4)
deployer/service.py (+14/-10)
deployer/tests/base.py (+5/-2)
deployer/tests/mock.py (+0/-2367)
deployer/tests/test_base.py (+4/-2)
deployer/tests/test_charm.py (+2/-1)
deployer/tests/test_config.py (+20/-12)
deployer/tests/test_constraints.py (+8/-5)
deployer/tests/test_deployment.py (+19/-11)
deployer/tests/test_diff.py (+8/-5)
deployer/tests/test_goenv.py (+2/-1)
deployer/tests/test_guienv.py (+1/-0)
deployer/tests/test_guiserver.py (+1/-0)
deployer/tests/test_importer.py (+10/-7)
deployer/tests/test_pyenv.py (+4/-2)
deployer/tests/test_service.py (+1/-0)
deployer/tests/test_utils.py (+13/-10)
deployer/tests/test_watchers.py (+16/-7)
deployer/utils.py (+11/-5)
deployer/vcs.py (+6/-8)
doc/conf.py (+1/-0)
setup.py (+6/-1)
test-requirements.txt (+4/-0)
tox.ini (+20/-0)
To merge this branch: bzr merge lp:~tvansteenburgh/juju-deployer/python3-support
Reviewer Review Type Date Requested Status
juju-deployers Pending
Review via email: mp+294070@code.launchpad.net

Description of the change

Python 3 support.

All tests passing on python 2.7 and python 3.5 on Xenial (76% coverage). I successfully deployed a bundle using py2.7 and py3.5 on both juju-1.25.5 and juju-2.0-beta6.

These are the configurations I had readily available. If anyone can test others (trusty, py3.4, and especially juju-2.0-beta7), that would be helpful.

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file '.bzrignore'
--- .bzrignore 2015-02-20 19:18:58 +0000
+++ .bzrignore 2016-05-08 03:36:27 +0000
@@ -12,3 +12,4 @@
12tmp12tmp
13report/13report/
14.coverage14.coverage
15.tox/
1516
=== modified file 'HACKING'
--- HACKING 2016-04-04 18:22:59 +0000
+++ HACKING 2016-05-08 03:36:27 +0000
@@ -13,9 +13,10 @@
13Running unit tests13Running unit tests
14------------------14------------------
1515
16Tests are compatible with nose and can be run using, e.g.:16Tests can be run using tox, e.g.:
1717
18 $ nosetests -s --verbosity=2 deployer/tests18 $ tox # run tests for all supported python versions
19 $ tox -e py27 # run tests on python2.7
1920
2021
21Running live environment tests22Running live environment tests
2223
=== modified file 'Makefile'
--- Makefile 2014-08-26 22:34:07 +0000
+++ Makefile 2016-05-08 03:36:27 +0000
@@ -1,10 +1,2 @@
1test:1test:
2 nosetests -s --verbosity=2 deployer/tests2 tox
3
4freeze:
5 pip install -d tools/dist -r requirements.txt
6
7cover:
8 pip install coverage nose
9 nosetests --cover-html-dir=report --cover-html --with-coverage --cover-package=deployer
10 gnome-open report/index.html
113
=== modified file 'deployer/action/diff.py'
--- deployer/action/diff.py 2015-01-27 15:14:00 +0000
+++ deployer/action/diff.py 2016-05-08 03:36:27 +0000
@@ -1,9 +1,12 @@
1from __future__ import absolute_import
2from __future__ import print_function
1import logging3import logging
2import time4import time
35
4from .base import BaseAction6from .base import BaseAction
5from ..relation import EndpointPair7from ..relation import EndpointPair
6from ..utils import parse_constraints, yaml_dump8from ..utils import parse_constraints, yaml_dump
9import six
710
811
9class Diff(BaseAction):12class Diff(BaseAction):
@@ -22,7 +25,7 @@
22 """25 """
23 rels = set()26 rels = set()
24 for svc_name in self.env_status['services']:27 for svc_name in self.env_status['services']:
25 if not svc_name in self.env_status['services']:28 if svc_name not in self.env_status['services']:
26 self.env_state['services'][svc_name] = 'missing'29 self.env_state['services'][svc_name] = 'missing'
27 self.env_state['services'].setdefault(svc_name, {})[30 self.env_state['services'].setdefault(svc_name, {})[
28 'options'] = self.env.get_config(svc_name)31 'options'] = self.env.get_config(svc_name)
@@ -115,7 +118,7 @@
115 self.deployment.get_charm_for(cs))118 self.deployment.get_charm_for(cs))
116 if not mod:119 if not mod:
117 continue120 continue
118 if not 'modified' in delta:121 if 'modified' not in delta:
119 delta['modified'] = {}122 delta['modified'] = {}
120 delta['modified'][cs] = mod123 delta['modified'][cs] = mod
121 return delta124 return delta
@@ -125,7 +128,7 @@
125 d_sc = parse_constraints(d_s.get('constraints', ''))128 d_sc = parse_constraints(d_s.get('constraints', ''))
126 # 'tags' is a special case, as it can be multi-valued: convert to list129 # 'tags' is a special case, as it can be multi-valued: convert to list
127 # if cfg one is a string130 # if cfg one is a string
128 if isinstance(d_sc.get('tags'), basestring):131 if isinstance(d_sc.get('tags'), six.string_types):
129 d_sc['tags'] = [d_sc['tags']]132 d_sc['tags'] = [d_sc['tags']]
130 if d_sc != e_s['constraints']:133 if d_sc != e_s['constraints']:
131 mod['env-constraints'] = e_s['constraints']134 mod['env-constraints'] = e_s['constraints']
@@ -133,7 +136,7 @@
133 for k, v in d_s.get('options', {}).items():136 for k, v in d_s.get('options', {}).items():
134 # Deploy options not known to the env may originate137 # Deploy options not known to the env may originate
135 # from charm version delta or be an invalid config.138 # from charm version delta or be an invalid config.
136 if not k in e_s['options']:139 if k not in e_s['options']:
137 continue140 continue
138 e_v = e_s['options'].get(k, {}).get('value')141 e_v = e_s['options'].get(k, {}).get('value')
139 if e_v != v:142 if e_v != v:
@@ -155,4 +158,4 @@
155 def run(self):158 def run(self):
156 diff = self.do_diff()159 diff = self.do_diff()
157 if diff:160 if diff:
158 print yaml_dump(diff)161 print(yaml_dump(diff))
159162
=== modified file 'deployer/action/export.py'
--- deployer/action/export.py 2013-07-22 15:29:31 +0000
+++ deployer/action/export.py 2016-05-08 03:36:27 +0000
@@ -1,3 +1,4 @@
1from __future__ import absolute_import
1import logging2import logging
23
3from .base import BaseAction4from .base import BaseAction
45
=== modified file 'deployer/action/importer.py'
--- deployer/action/importer.py 2016-05-03 16:03:18 +0000
+++ deployer/action/importer.py 2016-05-08 03:36:27 +0000
@@ -1,9 +1,12 @@
1from __future__ import absolute_import
1import logging2import logging
2import time3import time
34
4from .base import BaseAction5from .base import BaseAction
5from ..env import watchers6from ..env import watchers
6from ..utils import ErrorExit7from ..utils import ErrorExit
8from six.moves import map
9from six.moves import range
710
811
9class Importer(BaseAction):12class Importer(BaseAction):
@@ -65,7 +68,7 @@
6568
66 def machine_exists(self, id):69 def machine_exists(self, id):
67 """Checks if the given id exists on the current environment."""70 """Checks if the given id exists on the current environment."""
68 return str(id) in map(str, self.env.status().get('machines', {}))71 return str(id) in list(map(str, self.env.status().get('machines', {})))
6972
70 def create_machines(self):73 def create_machines(self):
71 """Create machines as specified in the machine spec in the bundle.74 """Create machines as specified in the machine spec in the bundle.
7275
=== modified file 'deployer/charm.py'
--- deployer/charm.py 2016-05-03 16:03:18 +0000
+++ deployer/charm.py 2016-05-08 03:36:27 +0000
@@ -1,8 +1,10 @@
1from __future__ import absolute_import
1import logging2import logging
2import os3import os
3import urllib2
4import shutil4import shutil
55
6from six.moves.urllib.request import urlopen
7
6from .vcs import Git, Bzr8from .vcs import Git, Bzr
7from .utils import (9from .utils import (
8 _check_call,10 _check_call,
@@ -210,7 +212,7 @@
210212
211 store_url = "%s/charm/%s" % (STORE_URL, qualified_url[3:])213 store_url = "%s/charm/%s" % (STORE_URL, qualified_url[3:])
212 with temp_file() as fh:214 with temp_file() as fh:
213 ufh = urllib2.urlopen(store_url)215 ufh = urlopen(store_url)
214 shutil.copyfileobj(ufh, fh)216 shutil.copyfileobj(ufh, fh)
215 fh.flush()217 fh.flush()
216 extract_zip(fh.name, self.path)218 extract_zip(fh.name, self.path)
217219
=== modified file 'deployer/cli.py'
--- deployer/cli.py 2016-03-31 02:48:32 +0000
+++ deployer/cli.py 2016-05-08 03:36:27 +0000
@@ -7,6 +7,8 @@
7"""7"""
88
99
10from __future__ import absolute_import
11from __future__ import print_function
10import argparse12import argparse
11import errno13import errno
12import logging14import logging
@@ -220,14 +222,14 @@
220222
221 # Just list the available deployments223 # Just list the available deployments
222 if options.list_deploys:224 if options.list_deploys:
223 print("\n".join(sorted(config.keys())))225 print(("\n".join(sorted(config.keys()))))
224 sys.exit(0)226 sys.exit(0)
225227
226 # Do something to a deployment228 # Do something to a deployment
227 if not options.deployment:229 if not options.deployment:
228 # If there's only one option then use it.230 # If there's only one option then use it.
229 if len(config.keys()) == 1:231 if len(list(config.keys())) == 1:
230 options.deployment = config.keys()[0]232 options.deployment = list(config.keys())[0]
231 log.info("Using deployment %s", options.deployment)233 log.info("Using deployment %s", options.deployment)
232 else:234 else:
233 log.error(235 log.error(
234236
=== modified file 'deployer/config.py'
--- deployer/config.py 2015-08-05 15:39:40 +0000
+++ deployer/config.py 2016-05-08 03:36:27 +0000
@@ -1,12 +1,14 @@
1from __future__ import absolute_import
1from os.path import abspath, isabs, join, dirname2from os.path import abspath, isabs, join, dirname
23
3import logging4import logging
4import os5import os
5import tempfile6import tempfile
6import shutil7import shutil
7import urllib2
8import urlparse
98
9import six
10from six.moves.urllib.request import urlopen
11from six.moves.urllib.parse import urlparse
1012
11from .deployment import Deployment13from .deployment import Deployment
12from .utils import ErrorExit, yaml_load, path_exists, dict_merge14from .utils import ErrorExit, yaml_load, path_exists, dict_merge
@@ -23,14 +25,14 @@
23 self.data = {}25 self.data = {}
24 self.yaml = {}26 self.yaml = {}
25 self.include_dirs = []27 self.include_dirs = []
26 self.urlopen = urllib2.urlopen28 self.urlopen = urlopen
27 self.load()29 self.load()
2830
29 def _yaml_load(self, config_file):31 def _yaml_load(self, config_file):
30 if config_file in self.yaml:32 if config_file in self.yaml:
31 return self.yaml[config_file]33 return self.yaml[config_file]
3234
33 if urlparse.urlparse(config_file).scheme:35 if urlparse(config_file).scheme:
34 response = self.urlopen(config_file)36 response = self.urlopen(config_file)
35 if response.getcode() == 200:37 if response.getcode() == 200:
36 temp = tempfile.NamedTemporaryFile(delete=True)38 temp = tempfile.NamedTemporaryFile(delete=True)
@@ -44,7 +46,7 @@
44 with open(config_file) as fh:46 with open(config_file) as fh:
45 try:47 try:
46 yaml_result = yaml_load(fh.read())48 yaml_result = yaml_load(fh.read())
47 except Exception, e:49 except Exception as e:
48 self.log.warning(50 self.log.warning(
49 "Couldn't load config file @ %r, error: %s:%s",51 "Couldn't load config file @ %r, error: %s:%s",
50 config_file, type(e), e)52 config_file, type(e), e)
@@ -66,7 +68,7 @@
66 def get(self, key):68 def get(self, key):
67 if key not in self.data:69 if key not in self.data:
68 self.log.warning("Deployment %r not found. Available %s",70 self.log.warning("Deployment %r not found. Available %s",
69 key, ", ".join(self.keys()))71 key, ", ".join(list(self.keys())))
70 raise ErrorExit()72 raise ErrorExit()
71 deploy_data = self.data[key]73 deploy_data = self.data[key]
72 if self.version < 4:74 if self.version < 4:
@@ -94,7 +96,7 @@
9496
95 def _inherits(self, d):97 def _inherits(self, d):
96 parents = d.get('inherits', ())98 parents = d.get('inherits', ())
97 if isinstance(parents, basestring):99 if isinstance(parents, six.string_types):
98 parents = [parents]100 parents = [parents]
99 return parents101 return parents
100102
@@ -115,7 +117,7 @@
115 d = self._yaml_load(config_file)117 d = self._yaml_load(config_file)
116118
117 incs = d.get('include-configs') or d.get('include-config')119 incs = d.get('include-configs') or d.get('include-config')
118 if isinstance(incs, basestring):120 if isinstance(incs, six.string_types):
119 inc_fs = [incs]121 inc_fs = [incs]
120 else:122 else:
121 inc_fs = incs123 inc_fs = incs
122124
=== modified file 'deployer/deployment.py'
--- deployer/deployment.py 2015-08-06 12:04:16 +0000
+++ deployer/deployment.py 2016-05-08 03:36:27 +0000
@@ -1,10 +1,14 @@
1from __future__ import absolute_import
1from base64 import b64encode2from base64 import b64encode
3from functools import cmp_to_key
24
3import logging5import logging
4import pprint6import pprint
5import os7import os
6import yaml8import yaml
79
10import six
11
8from .charm import Charm12from .charm import Charm
9from .feedback import Feedback13from .feedback import Feedback
10from .service import Service, ServiceUnitPlacementV3, ServiceUnitPlacementV414from .service import Service, ServiceUnitPlacementV3, ServiceUnitPlacementV4
@@ -50,7 +54,7 @@
50 # Sort unplaced units first, then sort by name for placed units.54 # Sort unplaced units first, then sort by name for placed units.
51 services.sort(key=lambda svc: (bool(svc.unit_placement), svc.name))55 services.sort(key=lambda svc: (bool(svc.unit_placement), svc.name))
52 else:56 else:
53 services.sort(self._machines_placement_sort)57 services.sort(key=cmp_to_key(self._machines_placement_sort))
54 return services58 return services
5559
56 def set_machines(self, machines):60 def set_machines(self, machines):
@@ -78,7 +82,7 @@
7882
79 def get_service_names(self):83 def get_service_names(self):
80 """Return a sequence of service names for this deployment."""84 """Return a sequence of service names for this deployment."""
81 return self.data.get('services', {}).keys()85 return list(self.data.get('services', {}).keys())
8286
83 @staticmethod87 @staticmethod
84 def _machines_placement_sort(svc_a, svc_b):88 def _machines_placement_sort(svc_a, svc_b):
@@ -90,6 +94,9 @@
90 whether or not the service has a unit placement, and then finally94 whether or not the service has a unit placement, and then finally
91 based on the name of the service.95 based on the name of the service.
92 """96 """
97 def cmp_(a, b):
98 return (a > b) - (a < b)
99
93 if svc_a.unit_placement:100 if svc_a.unit_placement:
94 if svc_b.unit_placement:101 if svc_b.unit_placement:
95 # Check for colocation. This naively assumes that there is no102 # Check for colocation. This naively assumes that there is no
@@ -99,14 +106,14 @@
99 if x_in_y(svc_a, svc_b):106 if x_in_y(svc_a, svc_b):
100 return -1107 return -1
101 # If no colocation exists, simply compare names.108 # If no colocation exists, simply compare names.
102 return cmp(svc_a.name, svc_b.name)109 return cmp_(svc_a.name, svc_b.name)
103 return 1110 return 1
104 if svc_b.unit_placement:111 if svc_b.unit_placement:
105 return -1112 return -1
106 return cmp(svc_a.name, svc_b.name)113 return cmp_(svc_a.name, svc_b.name)
107114
108 def get_unit_placement(self, svc, status):115 def get_unit_placement(self, svc, status):
109 if isinstance(svc, (str, unicode)):116 if isinstance(svc, (str, six.text_type)):
110 svc = self.get_service(svc)117 svc = self.get_service(svc)
111 if self.version == 3:118 if self.version == 3:
112 return ServiceUnitPlacementV3(svc, self, status)119 return ServiceUnitPlacementV3(svc, self, status)
@@ -124,7 +131,6 @@
124 def check(a, b):131 def check(a, b):
125 k = tuple(sorted([a, b]))132 k = tuple(sorted([a, b]))
126 if k in seen:133 if k in seen:
127 #self.log.warning(" Skipping duplicate relation %r" % (k,))
128 return134 return
129 seen.add(k)135 seen.add(k)
130 return True136 return True
@@ -154,8 +160,6 @@
154 for r in rels[k]:160 for r in rels[k]:
155 if check(*r):161 if check(*r):
156 yield r162 yield r
157 #self.log.debug(
158 # "Found relations %s\n %s" % (" ".join(map(str, seen))))
159163
160 def get_charms(self):164 def get_charms(self):
161 for k, v in self.data.get('services', {}).items():165 for k, v in self.data.get('services', {}).items():
@@ -204,7 +208,7 @@
204 key, value = o.split('=', 1)208 key, value = o.split('=', 1)
205 overrides[key] = value209 overrides[key] = value
206210
207 for k, v in overrides.iteritems():211 for k, v in six.iteritems(overrides):
208 found = False212 found = False
209 for svc_name, svc_data in self.data['services'].items():213 for svc_name, svc_data in self.data['services'].items():
210 charm = self.get_charm_for(svc_name)214 charm = self.get_charm_for(svc_name)
@@ -225,7 +229,7 @@
225 # against defined services229 # against defined services
226 feedback = Feedback()230 feedback = Feedback()
227 for svc_name, svc_data in self.data.get('services', {}).items():231 for svc_name, svc_data in self.data.get('services', {}).items():
228 if not 'options' in svc_data:232 if 'options' not in svc_data:
229 continue233 continue
230 charm = self.get_charm_for(svc_name)234 charm = self.get_charm_for(svc_name)
231 config = charm.config235 config = charm.config
@@ -235,7 +239,7 @@
235 if svc_options is None:239 if svc_options is None:
236 svc_options = {}240 svc_options = {}
237 for k, v in svc_options.items():241 for k, v in svc_options.items():
238 if not k in config:242 if k not in config:
239 feedback.error(243 feedback.error(
240 "Invalid config charm %s %s=%s" % (charm.name, k, v))244 "Invalid config charm %s %s=%s" % (charm.name, k, v))
241 continue245 continue
@@ -252,18 +256,17 @@
252 def _resolve_include(self, svc_name, k, v):256 def _resolve_include(self, svc_name, k, v):
253 feedback = Feedback()257 feedback = Feedback()
254 for include_type in ["file", "base64"]:258 for include_type in ["file", "base64"]:
255 if (not isinstance(v, basestring)259 if (not isinstance(v, six.string_types) or
256 or not v.startswith(260 not v.startswith("include-%s://" % include_type)):
257 "include-%s://" % include_type)):
258 continue261 continue
259 include, fname = v.split("://", 1)262 include, fname = v.split("://", 1)
260 ip = resolve_include(fname, self.include_dirs)263 ip = resolve_include(fname, self.include_dirs)
261 if ip is None:264 if ip is None:
262 feedback.error(265 feedback.error(
263 "Invalid config %s.%s include not found %s" % (266 "Invalid config %s.%s include not found %s" % (
264 svc_name, k, v))267 svc_name, k, v))
265 continue268 continue
266 with open(ip) as fh:269 with open(ip, 'rb') as fh:
267 v = fh.read()270 v = fh.read()
268 if include_type == "base64":271 if include_type == "base64":
269 v = b64encode(v)272 v = b64encode(v)
@@ -277,7 +280,7 @@
277 feedback = Feedback()280 feedback = Feedback()
278 for e_a, e_b in self.get_relations():281 for e_a, e_b in self.get_relations():
279 for ep in [Endpoint(e_a), Endpoint(e_b)]:282 for ep in [Endpoint(e_a), Endpoint(e_b)]:
280 if not ep.service in services:283 if ep.service not in services:
281 feedback.error(284 feedback.error(
282 ("Invalid relation in config,"285 ("Invalid relation in config,"
283 " service %s not found, rel %s") % (286 " service %s not found, rel %s") % (
284287
=== modified file 'deployer/env/__init__.py'
--- deployer/env/__init__.py 2014-02-20 01:14:53 +0000
+++ deployer/env/__init__.py 2016-05-08 03:36:27 +0000
@@ -1,4 +1,3 @@
1#
2from .go import GoEnvironment1from .go import GoEnvironment
3from .py import PyEnvironment2from .py import PyEnvironment
4from ..utils import _check_call3from ..utils import _check_call
@@ -10,5 +9,3 @@
10 if result is None:9 if result is None:
11 return PyEnvironment(name, options)10 return PyEnvironment(name, options)
12 return GoEnvironment(name, options)11 return GoEnvironment(name, options)
13
14
1512
=== modified file 'deployer/env/base.py'
--- deployer/env/base.py 2016-05-05 17:39:13 +0000
+++ deployer/env/base.py 2016-05-08 03:36:27 +0000
@@ -1,3 +1,4 @@
1from __future__ import absolute_import
1import logging2import logging
23
3from ..utils import (4from ..utils import (
@@ -66,7 +67,7 @@
66 params = self._named_env(["juju", "deploy"])67 params = self._named_env(["juju", "deploy"])
67 with temp_file() as fh:68 with temp_file() as fh:
68 if config:69 if config:
69 fh.write(yaml_dump({name: config}))70 fh.write(yaml_dump({name: config}).encode())
70 fh.flush()71 fh.flush()
71 params.extend(["--config", fh.name])72 params.extend(["--config", fh.name])
72 if constraints:73 if constraints:
7374
=== modified file 'deployer/env/go.py'
--- deployer/env/go.py 2016-03-10 14:14:54 +0000
+++ deployer/env/go.py 2016-05-08 03:36:27 +0000
@@ -1,3 +1,5 @@
1from __future__ import absolute_import
2from functools import cmp_to_key
1import time3import time
24
3from .base import BaseEnvironment5from .base import BaseEnvironment
@@ -56,7 +58,6 @@
56 if self.client:58 if self.client:
57 self.client.close()59 self.client.close()
5860
59
60 def connect(self):61 def connect(self):
61 self.log.debug("Connecting to environment...")62 self.log.debug("Connecting to environment...")
62 if self.juju_version == 1:63 if self.juju_version == 1:
@@ -130,7 +131,7 @@
130 return131 return
131132
132 # containers before machines, container hosts post wait.133 # containers before machines, container hosts post wait.
133 machines = status['machines'].keys()134 machines = list(status['machines'].keys())
134135
135 container_hosts = set()136 container_hosts = set()
136 containers = set()137 containers = set()
@@ -145,9 +146,9 @@
145 return -1146 return -1
146 if m == y:147 if m == y:
147 return 1148 return 1
148 return cmp(x, y)149 return (x > y) - (x < y)
149150
150 machines.sort(machine_sort)151 machines.sort(key=cmp_to_key(machine_sort))
151152
152 for mid in machines:153 for mid in machines:
153 self._terminate_machine(mid, container_hosts, force=force)154 self._terminate_machine(mid, container_hosts, force=force)
154155
=== modified file 'deployer/env/mem.py'
--- deployer/env/mem.py 2014-04-21 22:49:05 +0000
+++ deployer/env/mem.py 2016-05-08 03:36:27 +0000
@@ -1,6 +1,8 @@
1from __future__ import absolute_import
1from deployer.utils import parse_constraints2from deployer.utils import parse_constraints
2from jujuclient import (UnitErrors,3from jujuclient import (UnitErrors,
3 EnvError)4 EnvError)
5from six.moves import range
46
57
6class MemoryEnvironment(object):8class MemoryEnvironment(object):
@@ -24,7 +26,7 @@
24 """Add units26 """Add units
25 """27 """
26 next_num = self._services_data[svc_name]['next_unit_num']28 next_num = self._services_data[svc_name]['next_unit_num']
27 for idx in xrange(next_num, next_num + num):29 for idx in range(next_num, next_num + num):
28 self._services[svc_name]['units'].append(30 self._services[svc_name]['units'].append(
29 '{}/{}'.format(svc_name, idx))31 '{}/{}'.format(svc_name, idx))
30 self._services_data[svc_name]['next_unit_num'] = \32 self._services_data[svc_name]['next_unit_num'] = \
@@ -44,7 +46,7 @@
44 def _get_service(self, svc_name):46 def _get_service(self, svc_name):
45 """ Get service by name (as returned by status())47 """ Get service by name (as returned by status())
46 """48 """
47 if not svc_name in self._services:49 if svc_name not in self._services:
48 raise EnvError("Invalid service name")50 raise EnvError("Invalid service name")
49 return self._services[svc_name]51 return self._services[svc_name]
5052
@@ -55,7 +57,7 @@
55 def destroy_service(self, svc_name):57 def destroy_service(self, svc_name):
56 """ Destroy a service58 """ Destroy a service
57 """59 """
58 if not svc_name in self._services:60 if svc_name not in self._services:
59 raise EnvError("Invalid service name")61 raise EnvError("Invalid service name")
60 del self._services[svc_name]62 del self._services[svc_name]
6163
6264
=== modified file 'deployer/env/py.py'
--- deployer/env/py.py 2014-02-18 12:16:46 +0000
+++ deployer/env/py.py 2016-05-08 03:36:27 +0000
@@ -1,8 +1,6 @@
1from __future__ import absolute_import
1import time2import time
23
3from deployer.errors import UnitErrors
4from deployer.utils import ErrorExit
5
6from .base import BaseEnvironment4from .base import BaseEnvironment
75
86
97
=== modified file 'deployer/env/watchers.py'
--- deployer/env/watchers.py 2016-03-31 20:08:08 +0000
+++ deployer/env/watchers.py 2016-05-08 03:36:27 +0000
@@ -1,8 +1,9 @@
1"""A collection of juju-core environment watchers."""1"""A collection of juju-core environment watchers."""
22
3from __future__ import absolute_import
3from jujuclient import WatchWrapper4from jujuclient import WatchWrapper
45
5from ..utils import ErrorExit, get_juju_major_version6from ..utils import ErrorExit
67
7# _status_map provides a translation of Juju 2 status codes to the closest8# _status_map provides a translation of Juju 2 status codes to the closest
8# Juju 1 equivalent. Only defines codes that need translation.9# Juju 1 equivalent. Only defines codes that need translation.
910
=== modified file 'deployer/errors.py'
--- deployer/errors.py 2013-07-22 15:29:31 +0000
+++ deployer/errors.py 2016-05-08 03:36:27 +0000
@@ -1,2 +1,4 @@
1# TODO make deployer specific exceptions, also move errorexit from utils to here.1# TODO make deployer specific exceptions,
2from jujuclient import UnitErrors, EnvError2# also move errorexit from utils to here.
3from __future__ import absolute_import
4from jujuclient import UnitErrors, EnvError # noqa
35
=== modified file 'deployer/feedback.py'
--- deployer/feedback.py 2013-11-20 05:08:15 +0000
+++ deployer/feedback.py 2016-05-08 03:36:27 +0000
@@ -21,6 +21,11 @@
21 return iter(self.messages)21 return iter(self.messages)
2222
23 def __nonzero__(self):23 def __nonzero__(self):
24 # py2
25 return bool(self.messages)
26
27 def __bool__(self):
28 # py3
24 return bool(self.messages)29 return bool(self.messages)
2530
26 def get_errors(self):31 def get_errors(self):
2732
=== modified file 'deployer/guiserver.py'
--- deployer/guiserver.py 2015-08-10 13:12:20 +0000
+++ deployer/guiserver.py 2016-05-08 03:36:27 +0000
@@ -9,6 +9,7 @@
9<https://code.launchpad.net/~juju-gui/charms/precise/juju-gui/trunk>.9<https://code.launchpad.net/~juju-gui/charms/precise/juju-gui/trunk>.
10"""10"""
1111
12from __future__ import absolute_import
12import os13import os
1314
14from deployer.action.importer import Importer15from deployer.action.importer import Importer
1516
=== modified file 'deployer/relation.py'
--- deployer/relation.py 2013-07-22 15:29:31 +0000
+++ deployer/relation.py 2016-05-08 03:36:27 +0000
@@ -1,3 +1,4 @@
1from __future__ import absolute_import
1import yaml2import yaml
23
34
@@ -23,13 +24,11 @@
23 def __eq__(self, ep_pair):24 def __eq__(self, ep_pair):
24 if not isinstance(ep_pair, EndpointPair):25 if not isinstance(ep_pair, EndpointPair):
25 return False26 return False
26 return (ep_pair.ep_x.service in self27 return (ep_pair.ep_x.service in self and
27 and
28 ep_pair.ep_y.service in self)28 ep_pair.ep_y.service in self)
2929
30 def __contains__(self, svc_name):30 def __contains__(self, svc_name):
31 return (svc_name == self.ep_x.service31 return (svc_name == self.ep_x.service or
32 or
33 svc_name == self.ep_y.service)32 svc_name == self.ep_y.service)
3433
35 def __hash__(self):34 def __hash__(self):
3635
=== modified file 'deployer/service.py'
--- deployer/service.py 2015-12-11 16:09:53 +0000
+++ deployer/service.py 2016-05-08 03:36:27 +0000
@@ -1,6 +1,8 @@
1from __future__ import absolute_import
1import itertools2import itertools
23
3from feedback import Feedback4from .feedback import Feedback
5from six.moves import map
46
57
6class Service(object):8class Service(object):
@@ -43,7 +45,7 @@
43 value = self.svc_data.get('force-machine')45 value = self.svc_data.get('force-machine')
44 if value is not None and not isinstance(value, list):46 if value is not None and not isinstance(value, list):
45 value = [value]47 value = [value]
46 return value and map(str, value) or []48 return value and list(map(str, value)) or []
4749
48 @property50 @property
49 def expose(self):51 def expose(self):
@@ -93,7 +95,7 @@
93 "Cannot solve, falling back to default placement",95 "Cannot solve, falling back to default placement",
94 svc.name, placement, u_idx)96 svc.name, placement, u_idx)
95 return None97 return None
96 unit_names = svc_units.keys()98 unit_names = list(svc_units.keys())
97 unit_names.sort()99 unit_names.sort()
98 machine = svc_units[unit_names[int(u_idx)]].get('machine')100 machine = svc_units[unit_names[int(u_idx)]].get('machine')
99 if not machine:101 if not machine:
@@ -125,7 +127,7 @@
125127
126 if not isinstance(unit_placement, list):128 if not isinstance(unit_placement, list):
127 unit_placement = [unit_placement]129 unit_placement = [unit_placement]
128 unit_placement = map(str, unit_placement)130 unit_placement = list(map(str, unit_placement))
129131
130 services = dict([(s.name, s) for s in self.deployment.get_services()])132 services = dict([(s.name, s) for s in self.deployment.get_services()])
131 machines = self.deployment.get_machines()133 machines = self.deployment.get_machines()
@@ -134,15 +136,15 @@
134 container, p, u_idx = self._parse_placement(p)136 container, p, u_idx = self._parse_placement(p)
135 if container and container not in ('lxc', 'kvm'):137 if container and container not in ('lxc', 'kvm'):
136 feedback.error(138 feedback.error(
137 "Invalid container type:%s service: %s placement: %s" \139 "Invalid container type:%s service: %s placement: %s"
138 % (container, self.service.name, unit_placement[idx]))140 % (container, self.service.name, unit_placement[idx]))
139 if u_idx:141 if u_idx:
140 if p in ('maas', 'zone'):142 if p in ('maas', 'zone'):
141 continue143 continue
142 if not u_idx.isdigit():144 if not u_idx.isdigit():
143 feedback.error(145 feedback.error(
144 "Invalid service:%s placement: %s" % (146 "Invalid service:%s placement: %s" % (
145 self.service.name, unit_placement[idx]))147 self.service.name, unit_placement[idx]))
146 if p.isdigit():148 if p.isdigit():
147 if p == '0' or p in machines or self.arbitrary_machines:149 if p == '0' or p in machines or self.arbitrary_machines:
148 continue150 continue
@@ -237,7 +239,8 @@
237239
238 self.service.svc_data['to'] = (240 self.service.svc_data['to'] = (
239 unit_mapping +241 unit_mapping +
240 list(itertools.repeat(unit_mapping[-1], unit_count - len(unit_mapping)))242 list(itertools.repeat(
243 unit_mapping[-1], unit_count - len(unit_mapping)))
241 )244 )
242 unit_mapping = self.service.unit_placement245 unit_mapping = self.service.unit_placement
243246
@@ -288,7 +291,7 @@
288291
289 if not isinstance(unit_placement, (list, tuple)):292 if not isinstance(unit_placement, (list, tuple)):
290 unit_placement = [unit_placement]293 unit_placement = [unit_placement]
291 unit_placement = map(str, unit_placement)294 unit_placement = list(map(str, unit_placement))
292295
293 services = dict([(s.name, s) for s in self.deployment.get_services()])296 services = dict([(s.name, s) for s in self.deployment.get_services()])
294 machines = self.deployment.get_machines()297 machines = self.deployment.get_machines()
@@ -349,7 +352,8 @@
349 # Generate a name for this machine to be used in the352 # Generate a name for this machine to be used in the
350 # machines_map used later; as a quick path forward, simply use353 # machines_map used later; as a quick path forward, simply use
351 # the unit's name.354 # the unit's name.
352 new_machines.append('{}/{}'.format(self.service.name, unit.next()))355 new_machines.append('{}/{}'.format(
356 self.service.name, next(unit)))
353 return new_machines357 return new_machines
354358
355 def get(self, unit_number):359 def get(self, unit_number):
356360
=== modified file 'deployer/tests/base.py'
--- deployer/tests/base.py 2015-08-06 13:02:14 +0000
+++ deployer/tests/base.py 2016-05-08 03:36:27 +0000
@@ -1,9 +1,9 @@
1from __future__ import absolute_import
1import inspect2import inspect
2import logging3import logging
3import os4import os
4import unittest5import unittest
5import shutil6import shutil
6import StringIO
7import tempfile7import tempfile
88
9import mock9import mock
@@ -11,6 +11,9 @@
11import deployer11import deployer
12from deployer.config import ConfigStack12from deployer.config import ConfigStack
1313
14from six import StringIO
15from six.moves import range
16
1417
15# Skip during launchpad recipe package builds (DEB_BUILD_ARCH) or if explicitly18# Skip during launchpad recipe package builds (DEB_BUILD_ARCH) or if explicitly
16# requested with 'TEST_OFFLINE=1'19# requested with 'TEST_OFFLINE=1'
@@ -48,7 +51,7 @@
48 def capture_logging(self, name="", level=logging.INFO,51 def capture_logging(self, name="", level=logging.INFO,
49 log_file=None, formatter=None):52 log_file=None, formatter=None):
50 if log_file is None:53 if log_file is None:
51 log_file = StringIO.StringIO()54 log_file = StringIO()
52 log_handler = logging.StreamHandler(log_file)55 log_handler = logging.StreamHandler(log_file)
53 if formatter:56 if formatter:
54 log_handler.setFormatter(formatter)57 log_handler.setFormatter(formatter)
5558
=== removed file 'deployer/tests/mock.py'
--- deployer/tests/mock.py 2013-11-22 20:43:40 +0000
+++ deployer/tests/mock.py 1970-01-01 00:00:00 +0000
@@ -1,2367 +0,0 @@
1# mock.py
2# Test tools for mocking and patching.
3# Copyright (C) 2007-2012 Michael Foord & the mock team
4# E-mail: fuzzyman AT voidspace DOT org DOT uk
5
6# mock 1.0
7# http://www.voidspace.org.uk/python/mock/
8
9# Released subject to the BSD License
10# Please see http://www.voidspace.org.uk/python/license.shtml
11
12# Scripts maintained at http://www.voidspace.org.uk/python/index.shtml
13# Comments, suggestions and bug reports welcome.
14
15
16__all__ = (
17 'Mock',
18 'MagicMock',
19 'patch',
20 'sentinel',
21 'DEFAULT',
22 'ANY',
23 'call',
24 'create_autospec',
25 'FILTER_DIR',
26 'NonCallableMock',
27 'NonCallableMagicMock',
28 'mock_open',
29 'PropertyMock',
30)
31
32
33__version__ = '1.0.1'
34
35
36import pprint
37import sys
38
39try:
40 import inspect
41except ImportError:
42 # for alternative platforms that
43 # may not have inspect
44 inspect = None
45
46try:
47 from functools import wraps as original_wraps
48except ImportError:
49 # Python 2.4 compatibility
50 def wraps(original):
51 def inner(f):
52 f.__name__ = original.__name__
53 f.__doc__ = original.__doc__
54 f.__module__ = original.__module__
55 f.__wrapped__ = original
56 return f
57 return inner
58else:
59 if sys.version_info[:2] >= (3, 3):
60 wraps = original_wraps
61 else:
62 def wraps(func):
63 def inner(f):
64 f = original_wraps(func)(f)
65 f.__wrapped__ = func
66 return f
67 return inner
68
69try:
70 unicode
71except NameError:
72 # Python 3
73 basestring = unicode = str
74
75try:
76 long
77except NameError:
78 # Python 3
79 long = int
80
81try:
82 BaseException
83except NameError:
84 # Python 2.4 compatibility
85 BaseException = Exception
86
87try:
88 next
89except NameError:
90 def next(obj):
91 return obj.next()
92
93
94BaseExceptions = (BaseException,)
95if 'java' in sys.platform:
96 # jython
97 import java
98 BaseExceptions = (BaseException, java.lang.Throwable)
99
100try:
101 _isidentifier = str.isidentifier
102except AttributeError:
103 # Python 2.X
104 import keyword
105 import re
106 regex = re.compile(r'^[a-z_][a-z0-9_]*$', re.I)
107 def _isidentifier(string):
108 if string in keyword.kwlist:
109 return False
110 return regex.match(string)
111
112
113inPy3k = sys.version_info[0] == 3
114
115# Needed to work around Python 3 bug where use of "super" interferes with
116# defining __class__ as a descriptor
117_super = super
118
119self = 'im_self'
120builtin = '__builtin__'
121if inPy3k:
122 self = '__self__'
123 builtin = 'builtins'
124
125FILTER_DIR = True
126
127
128def _is_instance_mock(obj):
129 # can't use isinstance on Mock objects because they override __class__
130 # The base class for all mocks is NonCallableMock
131 return issubclass(type(obj), NonCallableMock)
132
133
134def _is_exception(obj):
135 return (
136 isinstance(obj, BaseExceptions) or
137 isinstance(obj, ClassTypes) and issubclass(obj, BaseExceptions)
138 )
139
140
141class _slotted(object):
142 __slots__ = ['a']
143
144
145DescriptorTypes = (
146 type(_slotted.a),
147 property,
148)
149
150
151def _getsignature(func, skipfirst, instance=False):
152 if inspect is None:
153 raise ImportError('inspect module not available')
154
155 if isinstance(func, ClassTypes) and not instance:
156 try:
157 func = func.__init__
158 except AttributeError:
159 return
160 skipfirst = True
161 elif not isinstance(func, FunctionTypes):
162 # for classes where instance is True we end up here too
163 try:
164 func = func.__call__
165 except AttributeError:
166 return
167
168 if inPy3k:
169 try:
170 argspec = inspect.getfullargspec(func)
171 except TypeError:
172 # C function / method, possibly inherited object().__init__
173 return
174 regargs, varargs, varkw, defaults, kwonly, kwonlydef, ann = argspec
175 else:
176 try:
177 regargs, varargs, varkwargs, defaults = inspect.getargspec(func)
178 except TypeError:
179 # C function / method, possibly inherited object().__init__
180 return
181
182 # instance methods and classmethods need to lose the self argument
183 if getattr(func, self, None) is not None:
184 regargs = regargs[1:]
185 if skipfirst:
186 # this condition and the above one are never both True - why?
187 regargs = regargs[1:]
188
189 if inPy3k:
190 signature = inspect.formatargspec(
191 regargs, varargs, varkw, defaults,
192 kwonly, kwonlydef, ann, formatvalue=lambda value: "")
193 else:
194 signature = inspect.formatargspec(
195 regargs, varargs, varkwargs, defaults,
196 formatvalue=lambda value: "")
197 return signature[1:-1], func
198
199
200def _check_signature(func, mock, skipfirst, instance=False):
201 if not _callable(func):
202 return
203
204 result = _getsignature(func, skipfirst, instance)
205 if result is None:
206 return
207 signature, func = result
208
209 # can't use self because "self" is common as an argument name
210 # unfortunately even not in the first place
211 src = "lambda _mock_self, %s: None" % signature
212 checksig = eval(src, {})
213 _copy_func_details(func, checksig)
214 type(mock)._mock_check_sig = checksig
215
216
217def _copy_func_details(func, funcopy):
218 funcopy.__name__ = func.__name__
219 funcopy.__doc__ = func.__doc__
220 #funcopy.__dict__.update(func.__dict__)
221 funcopy.__module__ = func.__module__
222 if not inPy3k:
223 funcopy.func_defaults = func.func_defaults
224 return
225 funcopy.__defaults__ = func.__defaults__
226 funcopy.__kwdefaults__ = func.__kwdefaults__
227
228
229def _callable(obj):
230 if isinstance(obj, ClassTypes):
231 return True
232 if getattr(obj, '__call__', None) is not None:
233 return True
234 return False
235
236
237def _is_list(obj):
238 # checks for list or tuples
239 # XXXX badly named!
240 return type(obj) in (list, tuple)
241
242
243def _instance_callable(obj):
244 """Given an object, return True if the object is callable.
245 For classes, return True if instances would be callable."""
246 if not isinstance(obj, ClassTypes):
247 # already an instance
248 return getattr(obj, '__call__', None) is not None
249
250 klass = obj
251 # uses __bases__ instead of __mro__ so that we work with old style classes
252 if klass.__dict__.get('__call__') is not None:
253 return True
254
255 for base in klass.__bases__:
256 if _instance_callable(base):
257 return True
258 return False
259
260
261def _set_signature(mock, original, instance=False):
262 # creates a function with signature (*args, **kwargs) that delegates to a
263 # mock. It still does signature checking by calling a lambda with the same
264 # signature as the original.
265 if not _callable(original):
266 return
267
268 skipfirst = isinstance(original, ClassTypes)
269 result = _getsignature(original, skipfirst, instance)
270 if result is None:
271 # was a C function (e.g. object().__init__ ) that can't be mocked
272 return
273
274 signature, func = result
275
276 src = "lambda %s: None" % signature
277 checksig = eval(src, {})
278 _copy_func_details(func, checksig)
279
280 name = original.__name__
281 if not _isidentifier(name):
282 name = 'funcopy'
283 context = {'_checksig_': checksig, 'mock': mock}
284 src = """def %s(*args, **kwargs):
285 _checksig_(*args, **kwargs)
286 return mock(*args, **kwargs)""" % name
287 exec (src, context)
288 funcopy = context[name]
289 _setup_func(funcopy, mock)
290 return funcopy
291
292
293def _setup_func(funcopy, mock):
294 funcopy.mock = mock
295
296 # can't use isinstance with mocks
297 if not _is_instance_mock(mock):
298 return
299
300 def assert_called_with(*args, **kwargs):
301 return mock.assert_called_with(*args, **kwargs)
302 def assert_called_once_with(*args, **kwargs):
303 return mock.assert_called_once_with(*args, **kwargs)
304 def assert_has_calls(*args, **kwargs):
305 return mock.assert_has_calls(*args, **kwargs)
306 def assert_any_call(*args, **kwargs):
307 return mock.assert_any_call(*args, **kwargs)
308 def reset_mock():
309 funcopy.method_calls = _CallList()
310 funcopy.mock_calls = _CallList()
311 mock.reset_mock()
312 ret = funcopy.return_value
313 if _is_instance_mock(ret) and not ret is mock:
314 ret.reset_mock()
315
316 funcopy.called = False
317 funcopy.call_count = 0
318 funcopy.call_args = None
319 funcopy.call_args_list = _CallList()
320 funcopy.method_calls = _CallList()
321 funcopy.mock_calls = _CallList()
322
323 funcopy.return_value = mock.return_value
324 funcopy.side_effect = mock.side_effect
325 funcopy._mock_children = mock._mock_children
326
327 funcopy.assert_called_with = assert_called_with
328 funcopy.assert_called_once_with = assert_called_once_with
329 funcopy.assert_has_calls = assert_has_calls
330 funcopy.assert_any_call = assert_any_call
331 funcopy.reset_mock = reset_mock
332
333 mock._mock_delegate = funcopy
334
335
336def _is_magic(name):
337 return '__%s__' % name[2:-2] == name
338
339
340class _SentinelObject(object):
341 "A unique, named, sentinel object."
342 def __init__(self, name):
343 self.name = name
344
345 def __repr__(self):
346 return 'sentinel.%s' % self.name
347
348
349class _Sentinel(object):
350 """Access attributes to return a named object, usable as a sentinel."""
351 def __init__(self):
352 self._sentinels = {}
353
354 def __getattr__(self, name):
355 if name == '__bases__':
356 # Without this help(mock) raises an exception
357 raise AttributeError
358 return self._sentinels.setdefault(name, _SentinelObject(name))
359
360
361sentinel = _Sentinel()
362
363DEFAULT = sentinel.DEFAULT
364_missing = sentinel.MISSING
365_deleted = sentinel.DELETED
366
367
368class OldStyleClass:
369 pass
370ClassType = type(OldStyleClass)
371
372
373def _copy(value):
374 if type(value) in (dict, list, tuple, set):
375 return type(value)(value)
376 return value
377
378
379ClassTypes = (type,)
380if not inPy3k:
381 ClassTypes = (type, ClassType)
382
383_allowed_names = set(
384 [
385 'return_value', '_mock_return_value', 'side_effect',
386 '_mock_side_effect', '_mock_parent', '_mock_new_parent',
387 '_mock_name', '_mock_new_name'
388 ]
389)
390
391
392def _delegating_property(name):
393 _allowed_names.add(name)
394 _the_name = '_mock_' + name
395 def _get(self, name=name, _the_name=_the_name):
396 sig = self._mock_delegate
397 if sig is None:
398 return getattr(self, _the_name)
399 return getattr(sig, name)
400 def _set(self, value, name=name, _the_name=_the_name):
401 sig = self._mock_delegate
402 if sig is None:
403 self.__dict__[_the_name] = value
404 else:
405 setattr(sig, name, value)
406
407 return property(_get, _set)
408
409
410
411class _CallList(list):
412
413 def __contains__(self, value):
414 if not isinstance(value, list):
415 return list.__contains__(self, value)
416 len_value = len(value)
417 len_self = len(self)
418 if len_value > len_self:
419 return False
420
421 for i in range(0, len_self - len_value + 1):
422 sub_list = self[i:i+len_value]
423 if sub_list == value:
424 return True
425 return False
426
427 def __repr__(self):
428 return pprint.pformat(list(self))
429
430
431def _check_and_set_parent(parent, value, name, new_name):
432 if not _is_instance_mock(value):
433 return False
434 if ((value._mock_name or value._mock_new_name) or
435 (value._mock_parent is not None) or
436 (value._mock_new_parent is not None)):
437 return False
438
439 _parent = parent
440 while _parent is not None:
441 # setting a mock (value) as a child or return value of itself
442 # should not modify the mock
443 if _parent is value:
444 return False
445 _parent = _parent._mock_new_parent
446
447 if new_name:
448 value._mock_new_parent = parent
449 value._mock_new_name = new_name
450 if name:
451 value._mock_parent = parent
452 value._mock_name = name
453 return True
454
455
456
457class Base(object):
458 _mock_return_value = DEFAULT
459 _mock_side_effect = None
460 def __init__(self, *args, **kwargs):
461 pass
462
463
464
465class NonCallableMock(Base):
466 """A non-callable version of `Mock`"""
467
468 def __new__(cls, *args, **kw):
469 # every instance has its own class
470 # so we can create magic methods on the
471 # class without stomping on other mocks
472 new = type(cls.__name__, (cls,), {'__doc__': cls.__doc__})
473 instance = object.__new__(new)
474 return instance
475
476
477 def __init__(
478 self, spec=None, wraps=None, name=None, spec_set=None,
479 parent=None, _spec_state=None, _new_name='', _new_parent=None,
480 **kwargs
481 ):
482 if _new_parent is None:
483 _new_parent = parent
484
485 __dict__ = self.__dict__
486 __dict__['_mock_parent'] = parent
487 __dict__['_mock_name'] = name
488 __dict__['_mock_new_name'] = _new_name
489 __dict__['_mock_new_parent'] = _new_parent
490
491 if spec_set is not None:
492 spec = spec_set
493 spec_set = True
494
495 self._mock_add_spec(spec, spec_set)
496
497 __dict__['_mock_children'] = {}
498 __dict__['_mock_wraps'] = wraps
499 __dict__['_mock_delegate'] = None
500
501 __dict__['_mock_called'] = False
502 __dict__['_mock_call_args'] = None
503 __dict__['_mock_call_count'] = 0
504 __dict__['_mock_call_args_list'] = _CallList()
505 __dict__['_mock_mock_calls'] = _CallList()
506
507 __dict__['method_calls'] = _CallList()
508
509 if kwargs:
510 self.configure_mock(**kwargs)
511
512 _super(NonCallableMock, self).__init__(
513 spec, wraps, name, spec_set, parent,
514 _spec_state
515 )
516
517
518 def attach_mock(self, mock, attribute):
519 """
520 Attach a mock as an attribute of this one, replacing its name and
521 parent. Calls to the attached mock will be recorded in the
522 `method_calls` and `mock_calls` attributes of this one."""
523 mock._mock_parent = None
524 mock._mock_new_parent = None
525 mock._mock_name = ''
526 mock._mock_new_name = None
527
528 setattr(self, attribute, mock)
529
530
531 def mock_add_spec(self, spec, spec_set=False):
532 """Add a spec to a mock. `spec` can either be an object or a
533 list of strings. Only attributes on the `spec` can be fetched as
534 attributes from the mock.
535
536 If `spec_set` is True then only attributes on the spec can be set."""
537 self._mock_add_spec(spec, spec_set)
538
539
540 def _mock_add_spec(self, spec, spec_set):
541 _spec_class = None
542
543 if spec is not None and not _is_list(spec):
544 if isinstance(spec, ClassTypes):
545 _spec_class = spec
546 else:
547 _spec_class = _get_class(spec)
548
549 spec = dir(spec)
550
551 __dict__ = self.__dict__
552 __dict__['_spec_class'] = _spec_class
553 __dict__['_spec_set'] = spec_set
554 __dict__['_mock_methods'] = spec
555
556
557 def __get_return_value(self):
558 ret = self._mock_return_value
559 if self._mock_delegate is not None:
560 ret = self._mock_delegate.return_value
561
562 if ret is DEFAULT:
563 ret = self._get_child_mock(
564 _new_parent=self, _new_name='()'
565 )
566 self.return_value = ret
567 return ret
568
569
570 def __set_return_value(self, value):
571 if self._mock_delegate is not None:
572 self._mock_delegate.return_value = value
573 else:
574 self._mock_return_value = value
575 _check_and_set_parent(self, value, None, '()')
576
577 __return_value_doc = "The value to be returned when the mock is called."
578 return_value = property(__get_return_value, __set_return_value,
579 __return_value_doc)
580
581
582 @property
583 def __class__(self):
584 if self._spec_class is None:
585 return type(self)
586 return self._spec_class
587
588 called = _delegating_property('called')
589 call_count = _delegating_property('call_count')
590 call_args = _delegating_property('call_args')
591 call_args_list = _delegating_property('call_args_list')
592 mock_calls = _delegating_property('mock_calls')
593
594
595 def __get_side_effect(self):
596 sig = self._mock_delegate
597 if sig is None:
598 return self._mock_side_effect
599 return sig.side_effect
600
601 def __set_side_effect(self, value):
602 value = _try_iter(value)
603 sig = self._mock_delegate
604 if sig is None:
605 self._mock_side_effect = value
606 else:
607 sig.side_effect = value
608
609 side_effect = property(__get_side_effect, __set_side_effect)
610
611
612 def reset_mock(self):
613 "Restore the mock object to its initial state."
614 self.called = False
615 self.call_args = None
616 self.call_count = 0
617 self.mock_calls = _CallList()
618 self.call_args_list = _CallList()
619 self.method_calls = _CallList()
620
621 for child in self._mock_children.values():
622 if isinstance(child, _SpecState):
623 continue
624 child.reset_mock()
625
626 ret = self._mock_return_value
627 if _is_instance_mock(ret) and ret is not self:
628 ret.reset_mock()
629
630
631 def configure_mock(self, **kwargs):
632 """Set attributes on the mock through keyword arguments.
633
634 Attributes plus return values and side effects can be set on child
635 mocks using standard dot notation and unpacking a dictionary in the
636 method call:
637
638 >>> attrs = {'method.return_value': 3, 'other.side_effect': KeyError}
639 >>> mock.configure_mock(**attrs)"""
640 for arg, val in sorted(kwargs.items(),
641 # we sort on the number of dots so that
642 # attributes are set before we set attributes on
643 # attributes
644 key=lambda entry: entry[0].count('.')):
645 args = arg.split('.')
646 final = args.pop()
647 obj = self
648 for entry in args:
649 obj = getattr(obj, entry)
650 setattr(obj, final, val)
651
652
653 def __getattr__(self, name):
654 if name == '_mock_methods':
655 raise AttributeError(name)
656 elif self._mock_methods is not None:
657 if name not in self._mock_methods or name in _all_magics:
658 raise AttributeError("Mock object has no attribute %r" % name)
659 elif _is_magic(name):
660 raise AttributeError(name)
661
662 result = self._mock_children.get(name)
663 if result is _deleted:
664 raise AttributeError(name)
665 elif result is None:
666 wraps = None
667 if self._mock_wraps is not None:
668 # XXXX should we get the attribute without triggering code
669 # execution?
670 wraps = getattr(self._mock_wraps, name)
671
672 result = self._get_child_mock(
673 parent=self, name=name, wraps=wraps, _new_name=name,
674 _new_parent=self
675 )
676 self._mock_children[name] = result
677
678 elif isinstance(result, _SpecState):
679 result = create_autospec(
680 result.spec, result.spec_set, result.instance,
681 result.parent, result.name
682 )
683 self._mock_children[name] = result
684
685 return result
686
687
688 def __repr__(self):
689 _name_list = [self._mock_new_name]
690 _parent = self._mock_new_parent
691 last = self
692
693 dot = '.'
694 if _name_list == ['()']:
695 dot = ''
696 seen = set()
697 while _parent is not None:
698 last = _parent
699
700 _name_list.append(_parent._mock_new_name + dot)
701 dot = '.'
702 if _parent._mock_new_name == '()':
703 dot = ''
704
705 _parent = _parent._mock_new_parent
706
707 # use ids here so as not to call __hash__ on the mocks
708 if id(_parent) in seen:
709 break
710 seen.add(id(_parent))
711
712 _name_list = list(reversed(_name_list))
713 _first = last._mock_name or 'mock'
714 if len(_name_list) > 1:
715 if _name_list[1] not in ('()', '().'):
716 _first += '.'
717 _name_list[0] = _first
718 name = ''.join(_name_list)
719
720 name_string = ''
721 if name not in ('mock', 'mock.'):
722 name_string = ' name=%r' % name
723
724 spec_string = ''
725 if self._spec_class is not None:
726 spec_string = ' spec=%r'
727 if self._spec_set:
728 spec_string = ' spec_set=%r'
729 spec_string = spec_string % self._spec_class.__name__
730 return "<%s%s%s id='%s'>" % (
731 type(self).__name__,
732 name_string,
733 spec_string,
734 id(self)
735 )
736
737
738 def __dir__(self):
739 """Filter the output of `dir(mock)` to only useful members.
740 XXXX
741 """
742 extras = self._mock_methods or []
743 from_type = dir(type(self))
744 from_dict = list(self.__dict__)
745
746 if FILTER_DIR:
747 from_type = [e for e in from_type if not e.startswith('_')]
748 from_dict = [e for e in from_dict if not e.startswith('_') or
749 _is_magic(e)]
750 return sorted(set(extras + from_type + from_dict +
751 list(self._mock_children)))
752
753
754 def __setattr__(self, name, value):
755 if name in _allowed_names:
756 # property setters go through here
757 return object.__setattr__(self, name, value)
758 elif (self._spec_set and self._mock_methods is not None and
759 name not in self._mock_methods and
760 name not in self.__dict__):
761 raise AttributeError("Mock object has no attribute '%s'" % name)
762 elif name in _unsupported_magics:
763 msg = 'Attempting to set unsupported magic method %r.' % name
764 raise AttributeError(msg)
765 elif name in _all_magics:
766 if self._mock_methods is not None and name not in self._mock_methods:
767 raise AttributeError("Mock object has no attribute '%s'" % name)
768
769 if not _is_instance_mock(value):
770 setattr(type(self), name, _get_method(name, value))
771 original = value
772 value = lambda *args, **kw: original(self, *args, **kw)
773 else:
774 # only set _new_name and not name so that mock_calls is tracked
775 # but not method calls
776 _check_and_set_parent(self, value, None, name)
777 setattr(type(self), name, value)
778 self._mock_children[name] = value
779 elif name == '__class__':
780 self._spec_class = value
781 return
782 else:
783 if _check_and_set_parent(self, value, name, name):
784 self._mock_children[name] = value
785 return object.__setattr__(self, name, value)
786
787
788 def __delattr__(self, name):
789 if name in _all_magics and name in type(self).__dict__:
790 delattr(type(self), name)
791 if name not in self.__dict__:
792 # for magic methods that are still MagicProxy objects and
793 # not set on the instance itself
794 return
795
796 if name in self.__dict__:
797 object.__delattr__(self, name)
798
799 obj = self._mock_children.get(name, _missing)
800 if obj is _deleted:
801 raise AttributeError(name)
802 if obj is not _missing:
803 del self._mock_children[name]
804 self._mock_children[name] = _deleted
805
806
807
808 def _format_mock_call_signature(self, args, kwargs):
809 name = self._mock_name or 'mock'
810 return _format_call_signature(name, args, kwargs)
811
812
813 def _format_mock_failure_message(self, args, kwargs):
814 message = 'Expected call: %s\nActual call: %s'
815 expected_string = self._format_mock_call_signature(args, kwargs)
816 call_args = self.call_args
817 if len(call_args) == 3:
818 call_args = call_args[1:]
819 actual_string = self._format_mock_call_signature(*call_args)
820 return message % (expected_string, actual_string)
821
822
823 def assert_called_with(_mock_self, *args, **kwargs):
824 """assert that the mock was called with the specified arguments.
825
826 Raises an AssertionError if the args and keyword args passed in are
827 different to the last call to the mock."""
828 self = _mock_self
829 if self.call_args is None:
830 expected = self._format_mock_call_signature(args, kwargs)
831 raise AssertionError('Expected call: %s\nNot called' % (expected,))
832
833 if self.call_args != (args, kwargs):
834 msg = self._format_mock_failure_message(args, kwargs)
835 raise AssertionError(msg)
836
837
838 def assert_called_once_with(_mock_self, *args, **kwargs):
839 """assert that the mock was called exactly once and with the specified
840 arguments."""
841 self = _mock_self
842 if not self.call_count == 1:
843 msg = ("Expected to be called once. Called %s times." %
844 self.call_count)
845 raise AssertionError(msg)
846 return self.assert_called_with(*args, **kwargs)
847
848
849 def assert_has_calls(self, calls, any_order=False):
850 """assert the mock has been called with the specified calls.
851 The `mock_calls` list is checked for the calls.
852
853 If `any_order` is False (the default) then the calls must be
854 sequential. There can be extra calls before or after the
855 specified calls.
856
857 If `any_order` is True then the calls can be in any order, but
858 they must all appear in `mock_calls`."""
859 if not any_order:
860 if calls not in self.mock_calls:
861 raise AssertionError(
862 'Calls not found.\nExpected: %r\n'
863 'Actual: %r' % (calls, self.mock_calls)
864 )
865 return
866
867 all_calls = list(self.mock_calls)
868
869 not_found = []
870 for kall in calls:
871 try:
872 all_calls.remove(kall)
873 except ValueError:
874 not_found.append(kall)
875 if not_found:
876 raise AssertionError(
877 '%r not all found in call list' % (tuple(not_found),)
878 )
879
880
881 def assert_any_call(self, *args, **kwargs):
882 """assert the mock has been called with the specified arguments.
883
884 The assert passes if the mock has *ever* been called, unlike
885 `assert_called_with` and `assert_called_once_with` that only pass if
886 the call is the most recent one."""
887 kall = call(*args, **kwargs)
888 if kall not in self.call_args_list:
889 expected_string = self._format_mock_call_signature(args, kwargs)
890 raise AssertionError(
891 '%s call not found' % expected_string
892 )
893
894
895 def _get_child_mock(self, **kw):
896 """Create the child mocks for attributes and return value.
897 By default child mocks will be the same type as the parent.
898 Subclasses of Mock may want to override this to customize the way
899 child mocks are made.
900
901 For non-callable mocks the callable variant will be used (rather than
902 any custom subclass)."""
903 _type = type(self)
904 if not issubclass(_type, CallableMixin):
905 if issubclass(_type, NonCallableMagicMock):
906 klass = MagicMock
907 elif issubclass(_type, NonCallableMock) :
908 klass = Mock
909 else:
910 klass = _type.__mro__[1]
911 return klass(**kw)
912
913
914
915def _try_iter(obj):
916 if obj is None:
917 return obj
918 if _is_exception(obj):
919 return obj
920 if _callable(obj):
921 return obj
922 try:
923 return iter(obj)
924 except TypeError:
925 # XXXX backwards compatibility
926 # but this will blow up on first call - so maybe we should fail early?
927 return obj
928
929
930
931class CallableMixin(Base):
932
933 def __init__(self, spec=None, side_effect=None, return_value=DEFAULT,
934 wraps=None, name=None, spec_set=None, parent=None,
935 _spec_state=None, _new_name='', _new_parent=None, **kwargs):
936 self.__dict__['_mock_return_value'] = return_value
937
938 _super(CallableMixin, self).__init__(
939 spec, wraps, name, spec_set, parent,
940 _spec_state, _new_name, _new_parent, **kwargs
941 )
942
943 self.side_effect = side_effect
944
945
946 def _mock_check_sig(self, *args, **kwargs):
947 # stub method that can be replaced with one with a specific signature
948 pass
949
950
951 def __call__(_mock_self, *args, **kwargs):
952 # can't use self in-case a function / method we are mocking uses self
953 # in the signature
954 _mock_self._mock_check_sig(*args, **kwargs)
955 return _mock_self._mock_call(*args, **kwargs)
956
957
958 def _mock_call(_mock_self, *args, **kwargs):
959 self = _mock_self
960 self.called = True
961 self.call_count += 1
962 self.call_args = _Call((args, kwargs), two=True)
963 self.call_args_list.append(_Call((args, kwargs), two=True))
964
965 _new_name = self._mock_new_name
966 _new_parent = self._mock_new_parent
967 self.mock_calls.append(_Call(('', args, kwargs)))
968
969 seen = set()
970 skip_next_dot = _new_name == '()'
971 do_method_calls = self._mock_parent is not None
972 name = self._mock_name
973 while _new_parent is not None:
974 this_mock_call = _Call((_new_name, args, kwargs))
975 if _new_parent._mock_new_name:
976 dot = '.'
977 if skip_next_dot:
978 dot = ''
979
980 skip_next_dot = False
981 if _new_parent._mock_new_name == '()':
982 skip_next_dot = True
983
984 _new_name = _new_parent._mock_new_name + dot + _new_name
985
986 if do_method_calls:
987 if _new_name == name:
988 this_method_call = this_mock_call
989 else:
990 this_method_call = _Call((name, args, kwargs))
991 _new_parent.method_calls.append(this_method_call)
992
993 do_method_calls = _new_parent._mock_parent is not None
994 if do_method_calls:
995 name = _new_parent._mock_name + '.' + name
996
997 _new_parent.mock_calls.append(this_mock_call)
998 _new_parent = _new_parent._mock_new_parent
999
1000 # use ids here so as not to call __hash__ on the mocks
1001 _new_parent_id = id(_new_parent)
1002 if _new_parent_id in seen:
1003 break
1004 seen.add(_new_parent_id)
1005
1006 ret_val = DEFAULT
1007 effect = self.side_effect
1008 if effect is not None:
1009 if _is_exception(effect):
1010 raise effect
1011
1012 if not _callable(effect):
1013 result = next(effect)
1014 if _is_exception(result):
1015 raise result
1016 return result
1017
1018 ret_val = effect(*args, **kwargs)
1019 if ret_val is DEFAULT:
1020 ret_val = self.return_value
1021
1022 if (self._mock_wraps is not None and
1023 self._mock_return_value is DEFAULT):
1024 return self._mock_wraps(*args, **kwargs)
1025 if ret_val is DEFAULT:
1026 ret_val = self.return_value
1027 return ret_val
1028
1029
1030
1031class Mock(CallableMixin, NonCallableMock):
1032 """
1033 Create a new `Mock` object. `Mock` takes several optional arguments
1034 that specify the behaviour of the Mock object:
1035
1036 * `spec`: This can be either a list of strings or an existing object (a
1037 class or instance) that acts as the specification for the mock object. If
1038 you pass in an object then a list of strings is formed by calling dir on
1039 the object (excluding unsupported magic attributes and methods). Accessing
1040 any attribute not in this list will raise an `AttributeError`.
1041
1042 If `spec` is an object (rather than a list of strings) then
1043 `mock.__class__` returns the class of the spec object. This allows mocks
1044 to pass `isinstance` tests.
1045
1046 * `spec_set`: A stricter variant of `spec`. If used, attempting to *set*
1047 or get an attribute on the mock that isn't on the object passed as
1048 `spec_set` will raise an `AttributeError`.
1049
1050 * `side_effect`: A function to be called whenever the Mock is called. See
1051 the `side_effect` attribute. Useful for raising exceptions or
1052 dynamically changing return values. The function is called with the same
1053 arguments as the mock, and unless it returns `DEFAULT`, the return
1054 value of this function is used as the return value.
1055
1056 Alternatively `side_effect` can be an exception class or instance. In
1057 this case the exception will be raised when the mock is called.
1058
1059 If `side_effect` is an iterable then each call to the mock will return
1060 the next value from the iterable. If any of the members of the iterable
1061 are exceptions they will be raised instead of returned.
1062
1063 * `return_value`: The value returned when the mock is called. By default
1064 this is a new Mock (created on first access). See the
1065 `return_value` attribute.
1066
1067 * `wraps`: Item for the mock object to wrap. If `wraps` is not None then
1068 calling the Mock will pass the call through to the wrapped object
1069 (returning the real result). Attribute access on the mock will return a
1070 Mock object that wraps the corresponding attribute of the wrapped object
1071 (so attempting to access an attribute that doesn't exist will raise an
1072 `AttributeError`).
1073
1074 If the mock has an explicit `return_value` set then calls are not passed
1075 to the wrapped object and the `return_value` is returned instead.
1076
1077 * `name`: If the mock has a name then it will be used in the repr of the
1078 mock. This can be useful for debugging. The name is propagated to child
1079 mocks.
1080
1081 Mocks can also be called with arbitrary keyword arguments. These will be
1082 used to set attributes on the mock after it is created.
1083 """
1084
1085
1086
1087def _dot_lookup(thing, comp, import_path):
1088 try:
1089 return getattr(thing, comp)
1090 except AttributeError:
1091 __import__(import_path)
1092 return getattr(thing, comp)
1093
1094
1095def _importer(target):
1096 components = target.split('.')
1097 import_path = components.pop(0)
1098 thing = __import__(import_path)
1099
1100 for comp in components:
1101 import_path += ".%s" % comp
1102 thing = _dot_lookup(thing, comp, import_path)
1103 return thing
1104
1105
1106def _is_started(patcher):
1107 # XXXX horrible
1108 return hasattr(patcher, 'is_local')
1109
1110
1111class _patch(object):
1112
1113 attribute_name = None
1114 _active_patches = set()
1115
1116 def __init__(
1117 self, getter, attribute, new, spec, create,
1118 spec_set, autospec, new_callable, kwargs
1119 ):
1120 if new_callable is not None:
1121 if new is not DEFAULT:
1122 raise ValueError(
1123 "Cannot use 'new' and 'new_callable' together"
1124 )
1125 if autospec is not None:
1126 raise ValueError(
1127 "Cannot use 'autospec' and 'new_callable' together"
1128 )
1129
1130 self.getter = getter
1131 self.attribute = attribute
1132 self.new = new
1133 self.new_callable = new_callable
1134 self.spec = spec
1135 self.create = create
1136 self.has_local = False
1137 self.spec_set = spec_set
1138 self.autospec = autospec
1139 self.kwargs = kwargs
1140 self.additional_patchers = []
1141
1142
1143 def copy(self):
1144 patcher = _patch(
1145 self.getter, self.attribute, self.new, self.spec,
1146 self.create, self.spec_set,
1147 self.autospec, self.new_callable, self.kwargs
1148 )
1149 patcher.attribute_name = self.attribute_name
1150 patcher.additional_patchers = [
1151 p.copy() for p in self.additional_patchers
1152 ]
1153 return patcher
1154
1155
1156 def __call__(self, func):
1157 if isinstance(func, ClassTypes):
1158 return self.decorate_class(func)
1159 return self.decorate_callable(func)
1160
1161
1162 def decorate_class(self, klass):
1163 for attr in dir(klass):
1164 if not attr.startswith(patch.TEST_PREFIX):
1165 continue
1166
1167 attr_value = getattr(klass, attr)
1168 if not hasattr(attr_value, "__call__"):
1169 continue
1170
1171 patcher = self.copy()
1172 setattr(klass, attr, patcher(attr_value))
1173 return klass
1174
1175
1176 def decorate_callable(self, func):
1177 if hasattr(func, 'patchings'):
1178 func.patchings.append(self)
1179 return func
1180
1181 @wraps(func)
1182 def patched(*args, **keywargs):
1183 # don't use a with here (backwards compatability with Python 2.4)
1184 extra_args = []
1185 entered_patchers = []
1186
1187 # can't use try...except...finally because of Python 2.4
1188 # compatibility
1189 exc_info = tuple()
1190 try:
1191 try:
1192 for patching in patched.patchings:
1193 arg = patching.__enter__()
1194 entered_patchers.append(patching)
1195 if patching.attribute_name is not None:
1196 keywargs.update(arg)
1197 elif patching.new is DEFAULT:
1198 extra_args.append(arg)
1199
1200 args += tuple(extra_args)
1201 return func(*args, **keywargs)
1202 except:
1203 if (patching not in entered_patchers and
1204 _is_started(patching)):
1205 # the patcher may have been started, but an exception
1206 # raised whilst entering one of its additional_patchers
1207 entered_patchers.append(patching)
1208 # Pass the exception to __exit__
1209 exc_info = sys.exc_info()
1210 # re-raise the exception
1211 raise
1212 finally:
1213 for patching in reversed(entered_patchers):
1214 patching.__exit__(*exc_info)
1215
1216 patched.patchings = [self]
1217 if hasattr(func, 'func_code'):
1218 # not in Python 3
1219 patched.compat_co_firstlineno = getattr(
1220 func, "compat_co_firstlineno",
1221 func.func_code.co_firstlineno
1222 )
1223 return patched
1224
1225
1226 def get_original(self):
1227 target = self.getter()
1228 name = self.attribute
1229
1230 original = DEFAULT
1231 local = False
1232
1233 try:
1234 original = target.__dict__[name]
1235 except (AttributeError, KeyError):
1236 original = getattr(target, name, DEFAULT)
1237 else:
1238 local = True
1239
1240 if not self.create and original is DEFAULT:
1241 raise AttributeError(
1242 "%s does not have the attribute %r" % (target, name)
1243 )
1244 return original, local
1245
1246
1247 def __enter__(self):
1248 """Perform the patch."""
1249 new, spec, spec_set = self.new, self.spec, self.spec_set
1250 autospec, kwargs = self.autospec, self.kwargs
1251 new_callable = self.new_callable
1252 self.target = self.getter()
1253
1254 # normalise False to None
1255 if spec is False:
1256 spec = None
1257 if spec_set is False:
1258 spec_set = None
1259 if autospec is False:
1260 autospec = None
1261
1262 if spec is not None and autospec is not None:
1263 raise TypeError("Can't specify spec and autospec")
1264 if ((spec is not None or autospec is not None) and
1265 spec_set not in (True, None)):
1266 raise TypeError("Can't provide explicit spec_set *and* spec or autospec")
1267
1268 original, local = self.get_original()
1269
1270 if new is DEFAULT and autospec is None:
1271 inherit = False
1272 if spec is True:
1273 # set spec to the object we are replacing
1274 spec = original
1275 if spec_set is True:
1276 spec_set = original
1277 spec = None
1278 elif spec is not None:
1279 if spec_set is True:
1280 spec_set = spec
1281 spec = None
1282 elif spec_set is True:
1283 spec_set = original
1284
1285 if spec is not None or spec_set is not None:
1286 if original is DEFAULT:
1287 raise TypeError("Can't use 'spec' with create=True")
1288 if isinstance(original, ClassTypes):
1289 # If we're patching out a class and there is a spec
1290 inherit = True
1291
1292 Klass = MagicMock
1293 _kwargs = {}
1294 if new_callable is not None:
1295 Klass = new_callable
1296 elif spec is not None or spec_set is not None:
1297 this_spec = spec
1298 if spec_set is not None:
1299 this_spec = spec_set
1300 if _is_list(this_spec):
1301 not_callable = '__call__' not in this_spec
1302 else:
1303 not_callable = not _callable(this_spec)
1304 if not_callable:
1305 Klass = NonCallableMagicMock
1306
1307 if spec is not None:
1308 _kwargs['spec'] = spec
1309 if spec_set is not None:
1310 _kwargs['spec_set'] = spec_set
1311
1312 # add a name to mocks
1313 if (isinstance(Klass, type) and
1314 issubclass(Klass, NonCallableMock) and self.attribute):
1315 _kwargs['name'] = self.attribute
1316
1317 _kwargs.update(kwargs)
1318 new = Klass(**_kwargs)
1319
1320 if inherit and _is_instance_mock(new):
1321 # we can only tell if the instance should be callable if the
1322 # spec is not a list
1323 this_spec = spec
1324 if spec_set is not None:
1325 this_spec = spec_set
1326 if (not _is_list(this_spec) and not
1327 _instance_callable(this_spec)):
1328 Klass = NonCallableMagicMock
1329
1330 _kwargs.pop('name')
1331 new.return_value = Klass(_new_parent=new, _new_name='()',
1332 **_kwargs)
1333 elif autospec is not None:
1334 # spec is ignored, new *must* be default, spec_set is treated
1335 # as a boolean. Should we check spec is not None and that spec_set
1336 # is a bool?
1337 if new is not DEFAULT:
1338 raise TypeError(
1339 "autospec creates the mock for you. Can't specify "
1340 "autospec and new."
1341 )
1342 if original is DEFAULT:
1343 raise TypeError("Can't use 'autospec' with create=True")
1344 spec_set = bool(spec_set)
1345 if autospec is True:
1346 autospec = original
1347
1348 new = create_autospec(autospec, spec_set=spec_set,
1349 _name=self.attribute, **kwargs)
1350 elif kwargs:
1351 # can't set keyword args when we aren't creating the mock
1352 # XXXX If new is a Mock we could call new.configure_mock(**kwargs)
1353 raise TypeError("Can't pass kwargs to a mock we aren't creating")
1354
1355 new_attr = new
1356
1357 self.temp_original = original
1358 self.is_local = local
1359 setattr(self.target, self.attribute, new_attr)
1360 if self.attribute_name is not None:
1361 extra_args = {}
1362 if self.new is DEFAULT:
1363 extra_args[self.attribute_name] = new
1364 for patching in self.additional_patchers:
1365 arg = patching.__enter__()
1366 if patching.new is DEFAULT:
1367 extra_args.update(arg)
1368 return extra_args
1369
1370 return new
1371
1372
1373 def __exit__(self, *exc_info):
1374 """Undo the patch."""
1375 if not _is_started(self):
1376 raise RuntimeError('stop called on unstarted patcher')
1377
1378 if self.is_local and self.temp_original is not DEFAULT:
1379 setattr(self.target, self.attribute, self.temp_original)
1380 else:
1381 delattr(self.target, self.attribute)
1382 if not self.create and not hasattr(self.target, self.attribute):
1383 # needed for proxy objects like django settings
1384 setattr(self.target, self.attribute, self.temp_original)
1385
1386 del self.temp_original
1387 del self.is_local
1388 del self.target
1389 for patcher in reversed(self.additional_patchers):
1390 if _is_started(patcher):
1391 patcher.__exit__(*exc_info)
1392
1393
1394 def start(self):
1395 """Activate a patch, returning any created mock."""
1396 result = self.__enter__()
1397 self._active_patches.add(self)
1398 return result
1399
1400
1401 def stop(self):
1402 """Stop an active patch."""
1403 self._active_patches.discard(self)
1404 return self.__exit__()
1405
1406
1407
1408def _get_target(target):
1409 try:
1410 target, attribute = target.rsplit('.', 1)
1411 except (TypeError, ValueError):
1412 raise TypeError("Need a valid target to patch. You supplied: %r" %
1413 (target,))
1414 getter = lambda: _importer(target)
1415 return getter, attribute
1416
1417
1418def _patch_object(
1419 target, attribute, new=DEFAULT, spec=None,
1420 create=False, spec_set=None, autospec=None,
1421 new_callable=None, **kwargs
1422 ):
1423 """
1424 patch.object(target, attribute, new=DEFAULT, spec=None, create=False,
1425 spec_set=None, autospec=None, new_callable=None, **kwargs)
1426
1427 patch the named member (`attribute`) on an object (`target`) with a mock
1428 object.
1429
1430 `patch.object` can be used as a decorator, class decorator or a context
1431 manager. Arguments `new`, `spec`, `create`, `spec_set`,
1432 `autospec` and `new_callable` have the same meaning as for `patch`. Like
1433 `patch`, `patch.object` takes arbitrary keyword arguments for configuring
1434 the mock object it creates.
1435
1436 When used as a class decorator `patch.object` honours `patch.TEST_PREFIX`
1437 for choosing which methods to wrap.
1438 """
1439 getter = lambda: target
1440 return _patch(
1441 getter, attribute, new, spec, create,
1442 spec_set, autospec, new_callable, kwargs
1443 )
1444
1445
1446def _patch_multiple(target, spec=None, create=False, spec_set=None,
1447 autospec=None, new_callable=None, **kwargs):
1448 """Perform multiple patches in a single call. It takes the object to be
1449 patched (either as an object or a string to fetch the object by importing)
1450 and keyword arguments for the patches::
1451
1452 with patch.multiple(settings, FIRST_PATCH='one', SECOND_PATCH='two'):
1453 ...
1454
1455 Use `DEFAULT` as the value if you want `patch.multiple` to create
1456 mocks for you. In this case the created mocks are passed into a decorated
1457 function by keyword, and a dictionary is returned when `patch.multiple` is
1458 used as a context manager.
1459
1460 `patch.multiple` can be used as a decorator, class decorator or a context
1461 manager. The arguments `spec`, `spec_set`, `create`,
1462 `autospec` and `new_callable` have the same meaning as for `patch`. These
1463 arguments will be applied to *all* patches done by `patch.multiple`.
1464
1465 When used as a class decorator `patch.multiple` honours `patch.TEST_PREFIX`
1466 for choosing which methods to wrap.
1467 """
1468 if type(target) in (unicode, str):
1469 getter = lambda: _importer(target)
1470 else:
1471 getter = lambda: target
1472
1473 if not kwargs:
1474 raise ValueError(
1475 'Must supply at least one keyword argument with patch.multiple'
1476 )
1477 # need to wrap in a list for python 3, where items is a view
1478 items = list(kwargs.items())
1479 attribute, new = items[0]
1480 patcher = _patch(
1481 getter, attribute, new, spec, create, spec_set,
1482 autospec, new_callable, {}
1483 )
1484 patcher.attribute_name = attribute
1485 for attribute, new in items[1:]:
1486 this_patcher = _patch(
1487 getter, attribute, new, spec, create, spec_set,
1488 autospec, new_callable, {}
1489 )
1490 this_patcher.attribute_name = attribute
1491 patcher.additional_patchers.append(this_patcher)
1492 return patcher
1493
1494
1495def patch(
1496 target, new=DEFAULT, spec=None, create=False,
1497 spec_set=None, autospec=None, new_callable=None, **kwargs
1498 ):
1499 """
1500 `patch` acts as a function decorator, class decorator or a context
1501 manager. Inside the body of the function or with statement, the `target`
1502 is patched with a `new` object. When the function/with statement exits
1503 the patch is undone.
1504
1505 If `new` is omitted, then the target is replaced with a
1506 `MagicMock`. If `patch` is used as a decorator and `new` is
1507 omitted, the created mock is passed in as an extra argument to the
1508 decorated function. If `patch` is used as a context manager the created
1509 mock is returned by the context manager.
1510
1511 `target` should be a string in the form `'package.module.ClassName'`. The
1512 `target` is imported and the specified object replaced with the `new`
1513 object, so the `target` must be importable from the environment you are
1514 calling `patch` from. The target is imported when the decorated function
1515 is executed, not at decoration time.
1516
1517 The `spec` and `spec_set` keyword arguments are passed to the `MagicMock`
1518 if patch is creating one for you.
1519
1520 In addition you can pass `spec=True` or `spec_set=True`, which causes
1521 patch to pass in the object being mocked as the spec/spec_set object.
1522
1523 `new_callable` allows you to specify a different class, or callable object,
1524 that will be called to create the `new` object. By default `MagicMock` is
1525 used.
1526
1527 A more powerful form of `spec` is `autospec`. If you set `autospec=True`
1528 then the mock with be created with a spec from the object being replaced.
1529 All attributes of the mock will also have the spec of the corresponding
1530 attribute of the object being replaced. Methods and functions being
1531 mocked will have their arguments checked and will raise a `TypeError` if
1532 they are called with the wrong signature. For mocks replacing a class,
1533 their return value (the 'instance') will have the same spec as the class.
1534
1535 Instead of `autospec=True` you can pass `autospec=some_object` to use an
1536 arbitrary object as the spec instead of the one being replaced.
1537
1538 By default `patch` will fail to replace attributes that don't exist. If
1539 you pass in `create=True`, and the attribute doesn't exist, patch will
1540 create the attribute for you when the patched function is called, and
1541 delete it again afterwards. This is useful for writing tests against
1542 attributes that your production code creates at runtime. It is off by by
1543 default because it can be dangerous. With it switched on you can write
1544 passing tests against APIs that don't actually exist!
1545
1546 Patch can be used as a `TestCase` class decorator. It works by
1547 decorating each test method in the class. This reduces the boilerplate
1548 code when your test methods share a common patchings set. `patch` finds
1549 tests by looking for method names that start with `patch.TEST_PREFIX`.
1550 By default this is `test`, which matches the way `unittest` finds tests.
1551 You can specify an alternative prefix by setting `patch.TEST_PREFIX`.
1552
1553 Patch can be used as a context manager, with the with statement. Here the
1554 patching applies to the indented block after the with statement. If you
1555 use "as" then the patched object will be bound to the name after the
1556 "as"; very useful if `patch` is creating a mock object for you.
1557
1558 `patch` takes arbitrary keyword arguments. These will be passed to
1559 the `Mock` (or `new_callable`) on construction.
1560
1561 `patch.dict(...)`, `patch.multiple(...)` and `patch.object(...)` are
1562 available for alternate use-cases.
1563 """
1564 getter, attribute = _get_target(target)
1565 return _patch(
1566 getter, attribute, new, spec, create,
1567 spec_set, autospec, new_callable, kwargs
1568 )
1569
1570
1571class _patch_dict(object):
1572 """
1573 Patch a dictionary, or dictionary like object, and restore the dictionary
1574 to its original state after the test.
1575
1576 `in_dict` can be a dictionary or a mapping like container. If it is a
1577 mapping then it must at least support getting, setting and deleting items
1578 plus iterating over keys.
1579
1580 `in_dict` can also be a string specifying the name of the dictionary, which
1581 will then be fetched by importing it.
1582
1583 `values` can be a dictionary of values to set in the dictionary. `values`
1584 can also be an iterable of `(key, value)` pairs.
1585
1586 If `clear` is True then the dictionary will be cleared before the new
1587 values are set.
1588
1589 `patch.dict` can also be called with arbitrary keyword arguments to set
1590 values in the dictionary::
1591
1592 with patch.dict('sys.modules', mymodule=Mock(), other_module=Mock()):
1593 ...
1594
1595 `patch.dict` can be used as a context manager, decorator or class
1596 decorator. When used as a class decorator `patch.dict` honours
1597 `patch.TEST_PREFIX` for choosing which methods to wrap.
1598 """
1599
1600 def __init__(self, in_dict, values=(), clear=False, **kwargs):
1601 if isinstance(in_dict, basestring):
1602 in_dict = _importer(in_dict)
1603 self.in_dict = in_dict
1604 # support any argument supported by dict(...) constructor
1605 self.values = dict(values)
1606 self.values.update(kwargs)
1607 self.clear = clear
1608 self._original = None
1609
1610
1611 def __call__(self, f):
1612 if isinstance(f, ClassTypes):
1613 return self.decorate_class(f)
1614 @wraps(f)
1615 def _inner(*args, **kw):
1616 self._patch_dict()
1617 try:
1618 return f(*args, **kw)
1619 finally:
1620 self._unpatch_dict()
1621
1622 return _inner
1623
1624
1625 def decorate_class(self, klass):
1626 for attr in dir(klass):
1627 attr_value = getattr(klass, attr)
1628 if (attr.startswith(patch.TEST_PREFIX) and
1629 hasattr(attr_value, "__call__")):
1630 decorator = _patch_dict(self.in_dict, self.values, self.clear)
1631 decorated = decorator(attr_value)
1632 setattr(klass, attr, decorated)
1633 return klass
1634
1635
1636 def __enter__(self):
1637 """Patch the dict."""
1638 self._patch_dict()
1639
1640
1641 def _patch_dict(self):
1642 values = self.values
1643 in_dict = self.in_dict
1644 clear = self.clear
1645
1646 try:
1647 original = in_dict.copy()
1648 except AttributeError:
1649 # dict like object with no copy method
1650 # must support iteration over keys
1651 original = {}
1652 for key in in_dict:
1653 original[key] = in_dict[key]
1654 self._original = original
1655
1656 if clear:
1657 _clear_dict(in_dict)
1658
1659 try:
1660 in_dict.update(values)
1661 except AttributeError:
1662 # dict like object with no update method
1663 for key in values:
1664 in_dict[key] = values[key]
1665
1666
1667 def _unpatch_dict(self):
1668 in_dict = self.in_dict
1669 original = self._original
1670
1671 _clear_dict(in_dict)
1672
1673 try:
1674 in_dict.update(original)
1675 except AttributeError:
1676 for key in original:
1677 in_dict[key] = original[key]
1678
1679
1680 def __exit__(self, *args):
1681 """Unpatch the dict."""
1682 self._unpatch_dict()
1683 return False
1684
1685 start = __enter__
1686 stop = __exit__
1687
1688
1689def _clear_dict(in_dict):
1690 try:
1691 in_dict.clear()
1692 except AttributeError:
1693 keys = list(in_dict)
1694 for key in keys:
1695 del in_dict[key]
1696
1697
1698def _patch_stopall():
1699 """Stop all active patches."""
1700 for patch in list(_patch._active_patches):
1701 patch.stop()
1702
1703
1704patch.object = _patch_object
1705patch.dict = _patch_dict
1706patch.multiple = _patch_multiple
1707patch.stopall = _patch_stopall
1708patch.TEST_PREFIX = 'test'
1709
1710magic_methods = (
1711 "lt le gt ge eq ne "
1712 "getitem setitem delitem "
1713 "len contains iter "
1714 "hash str sizeof "
1715 "enter exit "
1716 "divmod neg pos abs invert "
1717 "complex int float index "
1718 "trunc floor ceil "
1719)
1720
1721numerics = "add sub mul div floordiv mod lshift rshift and xor or pow "
1722inplace = ' '.join('i%s' % n for n in numerics.split())
1723right = ' '.join('r%s' % n for n in numerics.split())
1724extra = ''
1725if inPy3k:
1726 extra = 'bool next '
1727else:
1728 extra = 'unicode long nonzero oct hex truediv rtruediv '
1729
1730# not including __prepare__, __instancecheck__, __subclasscheck__
1731# (as they are metaclass methods)
1732# __del__ is not supported at all as it causes problems if it exists
1733
1734_non_defaults = set('__%s__' % method for method in [
1735 'cmp', 'getslice', 'setslice', 'coerce', 'subclasses',
1736 'format', 'get', 'set', 'delete', 'reversed',
1737 'missing', 'reduce', 'reduce_ex', 'getinitargs',
1738 'getnewargs', 'getstate', 'setstate', 'getformat',
1739 'setformat', 'repr', 'dir'
1740])
1741
1742
1743def _get_method(name, func):
1744 "Turns a callable object (like a mock) into a real function"
1745 def method(self, *args, **kw):
1746 return func(self, *args, **kw)
1747 method.__name__ = name
1748 return method
1749
1750
1751_magics = set(
1752 '__%s__' % method for method in
1753 ' '.join([magic_methods, numerics, inplace, right, extra]).split()
1754)
1755
1756_all_magics = _magics | _non_defaults
1757
1758_unsupported_magics = set([
1759 '__getattr__', '__setattr__',
1760 '__init__', '__new__', '__prepare__'
1761 '__instancecheck__', '__subclasscheck__',
1762 '__del__'
1763])
1764
1765_calculate_return_value = {
1766 '__hash__': lambda self: object.__hash__(self),
1767 '__str__': lambda self: object.__str__(self),
1768 '__sizeof__': lambda self: object.__sizeof__(self),
1769 '__unicode__': lambda self: unicode(object.__str__(self)),
1770}
1771
1772_return_values = {
1773 '__lt__': NotImplemented,
1774 '__gt__': NotImplemented,
1775 '__le__': NotImplemented,
1776 '__ge__': NotImplemented,
1777 '__int__': 1,
1778 '__contains__': False,
1779 '__len__': 0,
1780 '__exit__': False,
1781 '__complex__': 1j,
1782 '__float__': 1.0,
1783 '__bool__': True,
1784 '__nonzero__': True,
1785 '__oct__': '1',
1786 '__hex__': '0x1',
1787 '__long__': long(1),
1788 '__index__': 1,
1789}
1790
1791
1792def _get_eq(self):
1793 def __eq__(other):
1794 ret_val = self.__eq__._mock_return_value
1795 if ret_val is not DEFAULT:
1796 return ret_val
1797 return self is other
1798 return __eq__
1799
1800def _get_ne(self):
1801 def __ne__(other):
1802 if self.__ne__._mock_return_value is not DEFAULT:
1803 return DEFAULT
1804 return self is not other
1805 return __ne__
1806
1807def _get_iter(self):
1808 def __iter__():
1809 ret_val = self.__iter__._mock_return_value
1810 if ret_val is DEFAULT:
1811 return iter([])
1812 # if ret_val was already an iterator, then calling iter on it should
1813 # return the iterator unchanged
1814 return iter(ret_val)
1815 return __iter__
1816
1817_side_effect_methods = {
1818 '__eq__': _get_eq,
1819 '__ne__': _get_ne,
1820 '__iter__': _get_iter,
1821}
1822
1823
1824
1825def _set_return_value(mock, method, name):
1826 fixed = _return_values.get(name, DEFAULT)
1827 if fixed is not DEFAULT:
1828 method.return_value = fixed
1829 return
1830
1831 return_calulator = _calculate_return_value.get(name)
1832 if return_calulator is not None:
1833 try:
1834 return_value = return_calulator(mock)
1835 except AttributeError:
1836 # XXXX why do we return AttributeError here?
1837 # set it as a side_effect instead?
1838 return_value = AttributeError(name)
1839 method.return_value = return_value
1840 return
1841
1842 side_effector = _side_effect_methods.get(name)
1843 if side_effector is not None:
1844 method.side_effect = side_effector(mock)
1845
1846
1847
1848class MagicMixin(object):
1849 def __init__(self, *args, **kw):
1850 _super(MagicMixin, self).__init__(*args, **kw)
1851 self._mock_set_magics()
1852
1853
1854 def _mock_set_magics(self):
1855 these_magics = _magics
1856
1857 if self._mock_methods is not None:
1858 these_magics = _magics.intersection(self._mock_methods)
1859
1860 remove_magics = set()
1861 remove_magics = _magics - these_magics
1862
1863 for entry in remove_magics:
1864 if entry in type(self).__dict__:
1865 # remove unneeded magic methods
1866 delattr(self, entry)
1867
1868 # don't overwrite existing attributes if called a second time
1869 these_magics = these_magics - set(type(self).__dict__)
1870
1871 _type = type(self)
1872 for entry in these_magics:
1873 setattr(_type, entry, MagicProxy(entry, self))
1874
1875
1876
1877class NonCallableMagicMock(MagicMixin, NonCallableMock):
1878 """A version of `MagicMock` that isn't callable."""
1879 def mock_add_spec(self, spec, spec_set=False):
1880 """Add a spec to a mock. `spec` can either be an object or a
1881 list of strings. Only attributes on the `spec` can be fetched as
1882 attributes from the mock.
1883
1884 If `spec_set` is True then only attributes on the spec can be set."""
1885 self._mock_add_spec(spec, spec_set)
1886 self._mock_set_magics()
1887
1888
1889
1890class MagicMock(MagicMixin, Mock):
1891 """
1892 MagicMock is a subclass of Mock with default implementations
1893 of most of the magic methods. You can use MagicMock without having to
1894 configure the magic methods yourself.
1895
1896 If you use the `spec` or `spec_set` arguments then *only* magic
1897 methods that exist in the spec will be created.
1898
1899 Attributes and the return value of a `MagicMock` will also be `MagicMocks`.
1900 """
1901 def mock_add_spec(self, spec, spec_set=False):
1902 """Add a spec to a mock. `spec` can either be an object or a
1903 list of strings. Only attributes on the `spec` can be fetched as
1904 attributes from the mock.
1905
1906 If `spec_set` is True then only attributes on the spec can be set."""
1907 self._mock_add_spec(spec, spec_set)
1908 self._mock_set_magics()
1909
1910
1911
1912class MagicProxy(object):
1913 def __init__(self, name, parent):
1914 self.name = name
1915 self.parent = parent
1916
1917 def __call__(self, *args, **kwargs):
1918 m = self.create_mock()
1919 return m(*args, **kwargs)
1920
1921 def create_mock(self):
1922 entry = self.name
1923 parent = self.parent
1924 m = parent._get_child_mock(name=entry, _new_name=entry,
1925 _new_parent=parent)
1926 setattr(parent, entry, m)
1927 _set_return_value(parent, m, entry)
1928 return m
1929
1930 def __get__(self, obj, _type=None):
1931 return self.create_mock()
1932
1933
1934
1935class _ANY(object):
1936 "A helper object that compares equal to everything."
1937
1938 def __eq__(self, other):
1939 return True
1940
1941 def __ne__(self, other):
1942 return False
1943
1944 def __repr__(self):
1945 return '<ANY>'
1946
1947ANY = _ANY()
1948
1949
1950
1951def _format_call_signature(name, args, kwargs):
1952 message = '%s(%%s)' % name
1953 formatted_args = ''
1954 args_string = ', '.join([repr(arg) for arg in args])
1955 kwargs_string = ', '.join([
1956 '%s=%r' % (key, value) for key, value in kwargs.items()
1957 ])
1958 if args_string:
1959 formatted_args = args_string
1960 if kwargs_string:
1961 if formatted_args:
1962 formatted_args += ', '
1963 formatted_args += kwargs_string
1964
1965 return message % formatted_args
1966
1967
1968
1969class _Call(tuple):
1970 """
1971 A tuple for holding the results of a call to a mock, either in the form
1972 `(args, kwargs)` or `(name, args, kwargs)`.
1973
1974 If args or kwargs are empty then a call tuple will compare equal to
1975 a tuple without those values. This makes comparisons less verbose::
1976
1977 _Call(('name', (), {})) == ('name',)
1978 _Call(('name', (1,), {})) == ('name', (1,))
1979 _Call(((), {'a': 'b'})) == ({'a': 'b'},)
1980
1981 The `_Call` object provides a useful shortcut for comparing with call::
1982
1983 _Call(((1, 2), {'a': 3})) == call(1, 2, a=3)
1984 _Call(('foo', (1, 2), {'a': 3})) == call.foo(1, 2, a=3)
1985
1986 If the _Call has no name then it will match any name.
1987 """
1988 def __new__(cls, value=(), name=None, parent=None, two=False,
1989 from_kall=True):
1990 name = ''
1991 args = ()
1992 kwargs = {}
1993 _len = len(value)
1994 if _len == 3:
1995 name, args, kwargs = value
1996 elif _len == 2:
1997 first, second = value
1998 if isinstance(first, basestring):
1999 name = first
2000 if isinstance(second, tuple):
2001 args = second
2002 else:
2003 kwargs = second
2004 else:
2005 args, kwargs = first, second
2006 elif _len == 1:
2007 value, = value
2008 if isinstance(value, basestring):
2009 name = value
2010 elif isinstance(value, tuple):
2011 args = value
2012 else:
2013 kwargs = value
2014
2015 if two:
2016 return tuple.__new__(cls, (args, kwargs))
2017
2018 return tuple.__new__(cls, (name, args, kwargs))
2019
2020
2021 def __init__(self, value=(), name=None, parent=None, two=False,
2022 from_kall=True):
2023 self.name = name
2024 self.parent = parent
2025 self.from_kall = from_kall
2026
2027
2028 def __eq__(self, other):
2029 if other is ANY:
2030 return True
2031 try:
2032 len_other = len(other)
2033 except TypeError:
2034 return False
2035
2036 self_name = ''
2037 if len(self) == 2:
2038 self_args, self_kwargs = self
2039 else:
2040 self_name, self_args, self_kwargs = self
2041
2042 other_name = ''
2043 if len_other == 0:
2044 other_args, other_kwargs = (), {}
2045 elif len_other == 3:
2046 other_name, other_args, other_kwargs = other
2047 elif len_other == 1:
2048 value, = other
2049 if isinstance(value, tuple):
2050 other_args = value
2051 other_kwargs = {}
2052 elif isinstance(value, basestring):
2053 other_name = value
2054 other_args, other_kwargs = (), {}
2055 else:
2056 other_args = ()
2057 other_kwargs = value
2058 else:
2059 # len 2
2060 # could be (name, args) or (name, kwargs) or (args, kwargs)
2061 first, second = other
2062 if isinstance(first, basestring):
2063 other_name = first
2064 if isinstance(second, tuple):
2065 other_args, other_kwargs = second, {}
2066 else:
2067 other_args, other_kwargs = (), second
2068 else:
2069 other_args, other_kwargs = first, second
2070
2071 if self_name and other_name != self_name:
2072 return False
2073
2074 # this order is important for ANY to work!
2075 return (other_args, other_kwargs) == (self_args, self_kwargs)
2076
2077
2078 def __ne__(self, other):
2079 return not self.__eq__(other)
2080
2081
2082 def __call__(self, *args, **kwargs):
2083 if self.name is None:
2084 return _Call(('', args, kwargs), name='()')
2085
2086 name = self.name + '()'
2087 return _Call((self.name, args, kwargs), name=name, parent=self)
2088
2089
2090 def __getattr__(self, attr):
2091 if self.name is None:
2092 return _Call(name=attr, from_kall=False)
2093 name = '%s.%s' % (self.name, attr)
2094 return _Call(name=name, parent=self, from_kall=False)
2095
2096
2097 def __repr__(self):
2098 if not self.from_kall:
2099 name = self.name or 'call'
2100 if name.startswith('()'):
2101 name = 'call%s' % name
2102 return name
2103
2104 if len(self) == 2:
2105 name = 'call'
2106 args, kwargs = self
2107 else:
2108 name, args, kwargs = self
2109 if not name:
2110 name = 'call'
2111 elif not name.startswith('()'):
2112 name = 'call.%s' % name
2113 else:
2114 name = 'call%s' % name
2115 return _format_call_signature(name, args, kwargs)
2116
2117
2118 def call_list(self):
2119 """For a call object that represents multiple calls, `call_list`
2120 returns a list of all the intermediate calls as well as the
2121 final call."""
2122 vals = []
2123 thing = self
2124 while thing is not None:
2125 if thing.from_kall:
2126 vals.append(thing)
2127 thing = thing.parent
2128 return _CallList(reversed(vals))
2129
2130
2131call = _Call(from_kall=False)
2132
2133
2134
2135def create_autospec(spec, spec_set=False, instance=False, _parent=None,
2136 _name=None, **kwargs):
2137 """Create a mock object using another object as a spec. Attributes on the
2138 mock will use the corresponding attribute on the `spec` object as their
2139 spec.
2140
2141 Functions or methods being mocked will have their arguments checked
2142 to check that they are called with the correct signature.
2143
2144 If `spec_set` is True then attempting to set attributes that don't exist
2145 on the spec object will raise an `AttributeError`.
2146
2147 If a class is used as a spec then the return value of the mock (the
2148 instance of the class) will have the same spec. You can use a class as the
2149 spec for an instance object by passing `instance=True`. The returned mock
2150 will only be callable if instances of the mock are callable.
2151
2152 `create_autospec` also takes arbitrary keyword arguments that are passed to
2153 the constructor of the created mock."""
2154 if _is_list(spec):
2155 # can't pass a list instance to the mock constructor as it will be
2156 # interpreted as a list of strings
2157 spec = type(spec)
2158
2159 is_type = isinstance(spec, ClassTypes)
2160
2161 _kwargs = {'spec': spec}
2162 if spec_set:
2163 _kwargs = {'spec_set': spec}
2164 elif spec is None:
2165 # None we mock with a normal mock without a spec
2166 _kwargs = {}
2167
2168 _kwargs.update(kwargs)
2169
2170 Klass = MagicMock
2171 if type(spec) in DescriptorTypes:
2172 # descriptors don't have a spec
2173 # because we don't know what type they return
2174 _kwargs = {}
2175 elif not _callable(spec):
2176 Klass = NonCallableMagicMock
2177 elif is_type and instance and not _instance_callable(spec):
2178 Klass = NonCallableMagicMock
2179
2180 _new_name = _name
2181 if _parent is None:
2182 # for a top level object no _new_name should be set
2183 _new_name = ''
2184
2185 mock = Klass(parent=_parent, _new_parent=_parent, _new_name=_new_name,
2186 name=_name, **_kwargs)
2187
2188 if isinstance(spec, FunctionTypes):
2189 # should only happen at the top level because we don't
2190 # recurse for functions
2191 mock = _set_signature(mock, spec)
2192 else:
2193 _check_signature(spec, mock, is_type, instance)
2194
2195 if _parent is not None and not instance:
2196 _parent._mock_children[_name] = mock
2197
2198 if is_type and not instance and 'return_value' not in kwargs:
2199 mock.return_value = create_autospec(spec, spec_set, instance=True,
2200 _name='()', _parent=mock)
2201
2202 for entry in dir(spec):
2203 if _is_magic(entry):
2204 # MagicMock already does the useful magic methods for us
2205 continue
2206
2207 if isinstance(spec, FunctionTypes) and entry in FunctionAttributes:
2208 # allow a mock to actually be a function
2209 continue
2210
2211 # XXXX do we need a better way of getting attributes without
2212 # triggering code execution (?) Probably not - we need the actual
2213 # object to mock it so we would rather trigger a property than mock
2214 # the property descriptor. Likewise we want to mock out dynamically
2215 # provided attributes.
2216 # XXXX what about attributes that raise exceptions other than
2217 # AttributeError on being fetched?
2218 # we could be resilient against it, or catch and propagate the
2219 # exception when the attribute is fetched from the mock
2220 try:
2221 original = getattr(spec, entry)
2222 except AttributeError:
2223 continue
2224
2225 kwargs = {'spec': original}
2226 if spec_set:
2227 kwargs = {'spec_set': original}
2228
2229 if not isinstance(original, FunctionTypes):
2230 new = _SpecState(original, spec_set, mock, entry, instance)
2231 mock._mock_children[entry] = new
2232 else:
2233 parent = mock
2234 if isinstance(spec, FunctionTypes):
2235 parent = mock.mock
2236
2237 new = MagicMock(parent=parent, name=entry, _new_name=entry,
2238 _new_parent=parent, **kwargs)
2239 mock._mock_children[entry] = new
2240 skipfirst = _must_skip(spec, entry, is_type)
2241 _check_signature(original, new, skipfirst=skipfirst)
2242
2243 # so functions created with _set_signature become instance attributes,
2244 # *plus* their underlying mock exists in _mock_children of the parent
2245 # mock. Adding to _mock_children may be unnecessary where we are also
2246 # setting as an instance attribute?
2247 if isinstance(new, FunctionTypes):
2248 setattr(mock, entry, new)
2249
2250 return mock
2251
2252
2253def _must_skip(spec, entry, is_type):
2254 if not isinstance(spec, ClassTypes):
2255 if entry in getattr(spec, '__dict__', {}):
2256 # instance attribute - shouldn't skip
2257 return False
2258 spec = spec.__class__
2259 if not hasattr(spec, '__mro__'):
2260 # old style class: can't have descriptors anyway
2261 return is_type
2262
2263 for klass in spec.__mro__:
2264 result = klass.__dict__.get(entry, DEFAULT)
2265 if result is DEFAULT:
2266 continue
2267 if isinstance(result, (staticmethod, classmethod)):
2268 return False
2269 return is_type
2270
2271 # shouldn't get here unless function is a dynamically provided attribute
2272 # XXXX untested behaviour
2273 return is_type
2274
2275
2276def _get_class(obj):
2277 try:
2278 return obj.__class__
2279 except AttributeError:
2280 # in Python 2, _sre.SRE_Pattern objects have no __class__
2281 return type(obj)
2282
2283
2284class _SpecState(object):
2285
2286 def __init__(self, spec, spec_set=False, parent=None,
2287 name=None, ids=None, instance=False):
2288 self.spec = spec
2289 self.ids = ids
2290 self.spec_set = spec_set
2291 self.parent = parent
2292 self.instance = instance
2293 self.name = name
2294
2295
2296FunctionTypes = (
2297 # python function
2298 type(create_autospec),
2299 # instance method
2300 type(ANY.__eq__),
2301 # unbound method
2302 type(_ANY.__eq__),
2303)
2304
2305FunctionAttributes = set([
2306 'func_closure',
2307 'func_code',
2308 'func_defaults',
2309 'func_dict',
2310 'func_doc',
2311 'func_globals',
2312 'func_name',
2313])
2314
2315
2316file_spec = None
2317
2318
2319def mock_open(mock=None, read_data=''):
2320 """
2321 A helper function to create a mock to replace the use of `open`. It works
2322 for `open` called directly or used as a context manager.
2323
2324 The `mock` argument is the mock object to configure. If `None` (the
2325 default) then a `MagicMock` will be created for you, with the API limited
2326 to methods or attributes available on standard file handles.
2327
2328 `read_data` is a string for the `read` method of the file handle to return.
2329 This is an empty string by default.
2330 """
2331 global file_spec
2332 if file_spec is None:
2333 # set on first use
2334 if inPy3k:
2335 import _io
2336 file_spec = list(set(dir(_io.TextIOWrapper)).union(set(dir(_io.BytesIO))))
2337 else:
2338 file_spec = file
2339
2340 if mock is None:
2341 mock = MagicMock(name='open', spec=open)
2342
2343 handle = MagicMock(spec=file_spec)
2344 handle.write.return_value = None
2345 handle.__enter__.return_value = handle
2346 handle.read.return_value = read_data
2347
2348 mock.return_value = handle
2349 return mock
2350
2351
2352class PropertyMock(Mock):
2353 """
2354 A mock intended to be used as a property, or other descriptor, on a class.
2355 `PropertyMock` provides `__get__` and `__set__` methods so you can specify
2356 a return value when it is fetched.
2357
2358 Fetching a `PropertyMock` instance from an object calls the mock, with
2359 no args. Setting it calls the mock with the value being set.
2360 """
2361 def _get_child_mock(self, **kwargs):
2362 return MagicMock(**kwargs)
2363
2364 def __get__(self, obj, obj_type):
2365 return self()
2366 def __set__(self, obj, val):
2367 self(val)
23680
=== modified file 'deployer/tests/test_base.py'
--- deployer/tests/test_base.py 2013-07-22 15:29:31 +0000
+++ deployer/tests/test_base.py 2016-05-08 03:36:27 +0000
@@ -1,17 +1,19 @@
1from __future__ import absolute_import
1import os2import os
2import shutil3import shutil
3import unittest4import unittest
4import StringIO
5import logging5import logging
6import tempfile6import tempfile
77
8from six import StringIO
9
810
9class Base(unittest.TestCase):11class Base(unittest.TestCase):
1012
11 def capture_logging(self, name="", level=logging.INFO,13 def capture_logging(self, name="", level=logging.INFO,
12 log_file=None, formatter=None):14 log_file=None, formatter=None):
13 if log_file is None:15 if log_file is None:
14 log_file = StringIO.StringIO()16 log_file = StringIO()
15 log_handler = logging.StreamHandler(log_file)17 log_handler = logging.StreamHandler(log_file)
16 if formatter:18 if formatter:
17 log_handler.setFormatter(formatter)19 log_handler.setFormatter(formatter)
1820
=== modified file 'deployer/tests/test_charm.py'
--- deployer/tests/test_charm.py 2016-03-10 17:59:21 +0000
+++ deployer/tests/test_charm.py 2016-05-08 03:36:27 +0000
@@ -1,3 +1,4 @@
1from __future__ import absolute_import
1import os2import os
2import logging3import logging
3import subprocess4import subprocess
@@ -313,5 +314,5 @@
313 Charm.from_service(314 Charm.from_service(
314 "scratch", self.repo_path, "precise", params)315 "scratch", self.repo_path, "precise", params)
315 self.fail("should have failed, vcs ambigious")316 self.fail("should have failed, vcs ambigious")
316 except ValueError, e:317 except ValueError as e:
317 self.assertIn("Could not determine vcs backend", str(e))318 self.assertIn("Could not determine vcs backend", str(e))
318319
=== modified file 'deployer/tests/test_config.py'
--- deployer/tests/test_config.py 2015-03-04 19:27:31 +0000
+++ deployer/tests/test_config.py 2016-05-08 03:36:27 +0000
@@ -1,9 +1,12 @@
1from __future__ import absolute_import
1import logging2import logging
2import mock3import mock
3import os4import os
4import tempfile5import tempfile
5import yaml6import yaml
67
8import six
9
7from deployer.deployment import Deployment10from deployer.deployment import Deployment
8from deployer.config import ConfigStack11from deployer.config import ConfigStack
9from deployer.utils import ErrorExit12from deployer.utils import ErrorExit
@@ -21,7 +24,7 @@
21 config = ConfigStack(['configs/ostack-testing-sample.cfg'])24 config = ConfigStack(['configs/ostack-testing-sample.cfg'])
22 config.load()25 config.load()
23 self.assertEqual(26 self.assertEqual(
24 config.keys(),27 list(config.keys()),
25 [u'openstack-precise-ec2',28 [u'openstack-precise-ec2',
26 u'openstack-precise-ec2-trunk',29 u'openstack-precise-ec2-trunk',
27 u'openstack-ubuntu-testing'])30 u'openstack-ubuntu-testing'])
@@ -36,7 +39,7 @@
36 os.path.join(self.test_data_dir, "stack-inherits.cfg")])39 os.path.join(self.test_data_dir, "stack-inherits.cfg")])
37 config.load()40 config.load()
38 self.assertEqual(41 self.assertEqual(
39 config.keys(),42 list(config.keys()),
40 [u'my-files-frontend-dev', u'wordpress'])43 [u'my-files-frontend-dev', u'wordpress'])
41 deployment = config.get("wordpress")44 deployment = config.get("wordpress")
42 self.assertTrue(deployment)45 self.assertTrue(deployment)
@@ -47,11 +50,11 @@
47 os.path.join(self.test_data_dir, 'v4', 'simple.yaml')])50 os.path.join(self.test_data_dir, 'v4', 'simple.yaml')])
48 config.load()51 config.load()
49 self.assertEqual(52 self.assertEqual(
50 config.keys(),53 list(config.keys()),
51 [os.path.join(self.test_data_dir, 'v4', 'simple.yaml')])54 [os.path.join(self.test_data_dir, 'v4', 'simple.yaml')])
52 with mock.patch('deployer.config.ConfigStack._resolve_inherited') \55 with mock.patch('deployer.config.ConfigStack._resolve_inherited') \
53 as mock_resolve:56 as mock_resolve:
54 deployment = config.get(config.keys()[0])57 deployment = config.get(list(config.keys())[0])
55 self.assertTrue(deployment)58 self.assertTrue(deployment)
56 self.assertFalse(mock_resolve.called)59 self.assertFalse(mock_resolve.called)
57 self.assertEqual(config.version, 4)60 self.assertEqual(config.version, 4)
@@ -62,7 +65,7 @@
62 config.load()65 config.load()
63 # ensure picked up stacks from both files66 # ensure picked up stacks from both files
64 self.assertEqual(67 self.assertEqual(
65 config.keys(),68 list(config.keys()),
66 [u'my-files-frontend-dev', u'wordpress'])69 [u'my-files-frontend-dev', u'wordpress'])
6770
68 # ensure inheritance was adhered to during cross-file load71 # ensure inheritance was adhered to during cross-file load
@@ -104,7 +107,7 @@
104 for key in ['include-config', 'include-configs']:107 for key in ['include-config', 'include-configs']:
105 test_conf[key] = includes108 test_conf[key] = includes
106 with tempfile.NamedTemporaryFile() as tmp_cfg:109 with tempfile.NamedTemporaryFile() as tmp_cfg:
107 tmp_cfg.write(yaml.dump(test_conf))110 tmp_cfg.write(yaml.dump(test_conf).encode())
108 tmp_cfg.flush()111 tmp_cfg.flush()
109 config = ConfigStack([tmp_cfg.name])112 config = ConfigStack([tmp_cfg.name])
110 self._test_multiple_inheritance(config)113 self._test_multiple_inheritance(config)
@@ -141,7 +144,9 @@
141 ex_rels = [('quantum-gateway', 'nova-cloud-controller'),144 ex_rels = [('quantum-gateway', 'nova-cloud-controller'),
142 ('quantum-gateway', 'mysql'),145 ('quantum-gateway', 'mysql'),
143 ('nova-cloud-controller', 'mysql')]146 ('nova-cloud-controller', 'mysql')]
144 self.assertEquals(ex_rels, list(deployment.get_relations()))147 self.assertEquals(
148 sorted(ex_rels),
149 sorted(list(deployment.get_relations())))
145150
146 def test_config_series_override(self):151 def test_config_series_override(self):
147 config = ConfigStack(['configs/wiki.yaml'], 'trusty')152 config = ConfigStack(['configs/wiki.yaml'], 'trusty')
@@ -166,18 +171,19 @@
166 config = ConfigStack([])171 config = ConfigStack([])
167 config.config_files = [CONFIG_URL]172 config.config_files = [CONFIG_URL]
168173
169 class FauxResponse(file):174 class FauxResponse(six.BytesIO):
170 def getcode(self):175 def getcode(self):
171 return 200176 return 200
172177
173 def faux_urlopen(url):178 def faux_urlopen(url):
174 self.assertEqual(url, CONFIG_URL)179 self.assertEqual(url, CONFIG_URL)
175 return FauxResponse('configs/ostack-testing-sample.cfg')180 with open('configs/ostack-testing-sample.cfg', 'rb') as f:
181 return FauxResponse(f.read())
176182
177 config.urlopen = faux_urlopen183 config.urlopen = faux_urlopen
178 config.load()184 config.load()
179 self.assertEqual(185 self.assertEqual(
180 config.keys(),186 list(config.keys()),
181 [u'openstack-precise-ec2',187 [u'openstack-precise-ec2',
182 u'openstack-precise-ec2-trunk',188 u'openstack-precise-ec2-trunk',
183 u'openstack-ubuntu-testing'])189 u'openstack-ubuntu-testing'])
@@ -193,12 +199,14 @@
193 config = ConfigStack([])199 config = ConfigStack([])
194 config.config_files = [CONFIG_URL]200 config.config_files = [CONFIG_URL]
195201
196 class FauxResponse(file):202 class FauxResponse(six.BytesIO):
197 def getcode(self):203 def getcode(self):
198 return 400204 return 400
199205
200 def faux_urlopen(url):206 def faux_urlopen(url):
201 self.assertEqual(url, CONFIG_URL)207 self.assertEqual(url, CONFIG_URL)
202 return FauxResponse('configs/ostack-testing-sample.cfg')208 with open('configs/ostack-testing-sample.cfg', 'rb') as f:
209 return FauxResponse(f.read())
210
203 config.urlopen = faux_urlopen211 config.urlopen = faux_urlopen
204 self.assertRaises(ErrorExit, config.load)212 self.assertRaises(ErrorExit, config.load)
205213
=== modified file 'deployer/tests/test_constraints.py'
--- deployer/tests/test_constraints.py 2015-09-03 14:25:56 +0000
+++ deployer/tests/test_constraints.py 2016-05-08 03:36:27 +0000
@@ -1,3 +1,4 @@
1from __future__ import absolute_import
1from deployer.service import Service2from deployer.service import Service
2from .base import Base3from .base import Base
3from ..utils import parse_constraints4from ..utils import parse_constraints
@@ -94,9 +95,10 @@
94 'mem': '1E',95 'mem': '1E',
95 }96 }
96 with self.assertRaises(ValueError) as exc:97 with self.assertRaises(ValueError) as exc:
97 result = parse_constraints(value)98 parse_constraints(value)
98 self.assertEqual('Constraint mem has invalid value 1E',99 self.assertEqual(
99 exc.exception.message)100 'Constraint mem has invalid value 1E',
101 exc.exception.args[0])
100102
101 def test_other_numeric_constraints_have_no_units(self):103 def test_other_numeric_constraints_have_no_units(self):
102 # If any other numeric constraint gets a units specifier an error is104 # If any other numeric constraint gets a units specifier an error is
@@ -108,8 +110,9 @@
108 }110 }
109 with self.assertRaises(ValueError) as exc:111 with self.assertRaises(ValueError) as exc:
110 parse_constraints(value)112 parse_constraints(value)
111 self.assertEqual('Constraint {} has invalid value 1T'.format(k),113 self.assertEqual(
112 exc.exception.message)114 'Constraint {} has invalid value 1T'.format(k),
115 exc.exception.args[0])
113116
114 def test_non_numerics_are_not_converted(self):117 def test_non_numerics_are_not_converted(self):
115 # Constraints that expect strings are not affected by the parsing.118 # Constraints that expect strings are not affected by the parsing.
116119
=== modified file 'deployer/tests/test_deployment.py'
--- deployer/tests/test_deployment.py 2015-08-06 12:04:16 +0000
+++ deployer/tests/test_deployment.py 2016-05-08 03:36:27 +0000
@@ -1,7 +1,9 @@
1from __future__ import absolute_import
1import base642import base64
2import StringIO
3import os3import os
44
5from six import StringIO
6
5from deployer.deployment import Deployment7from deployer.deployment import Deployment
6from deployer.utils import setup_logging, ErrorExit8from deployer.utils import setup_logging, ErrorExit
79
@@ -22,7 +24,7 @@
2224
23 def setUp(self):25 def setUp(self):
24 self.output = setup_logging(26 self.output = setup_logging(
25 debug=True, verbose=True, stream=StringIO.StringIO())27 debug=True, verbose=True, stream=StringIO())
2628
27 def get_named_deployment_and_fetch_v3(self, file_name, stack_name):29 def get_named_deployment_and_fetch_v3(self, file_name, stack_name):
28 deployment = self.get_named_deployment_v3(file_name, stack_name)30 deployment = self.get_named_deployment_v3(file_name, stack_name)
@@ -46,7 +48,8 @@
4648
47 @skip_if_offline49 @skip_if_offline
48 def test_deployer(self):50 def test_deployer(self):
49 d = self.get_named_deployment_and_fetch_v3('blog.yaml', 'wordpress-prod')51 d = self.get_named_deployment_and_fetch_v3(
52 'blog.yaml', 'wordpress-prod')
50 services = d.get_services()53 services = d.get_services()
51 self.assertTrue([s for s in services if s.name == "newrelic"])54 self.assertTrue([s for s in services if s.name == "newrelic"])
5255
@@ -63,7 +66,7 @@
63 self.assertEqual(d.get_service('newrelic').config, {'key': 'abc'})66 self.assertEqual(d.get_service('newrelic').config, {'key': 'abc'})
64 self.assertEqual(67 self.assertEqual(
65 base64.b64decode(d.get_service('blog').config['wp-content']),68 base64.b64decode(d.get_service('blog').config['wp-content']),
66 "HelloWorld")69 b"HelloWorld")
6770
68 # TODO verify include-file71 # TODO verify include-file
6972
@@ -82,7 +85,8 @@
8285
83 @skip_if_offline86 @skip_if_offline
84 def test_validate_placement_sorting(self):87 def test_validate_placement_sorting(self):
85 d = self.get_named_deployment_and_fetch_v3("stack-placement.yaml", "stack")88 d = self.get_named_deployment_and_fetch_v3(
89 "stack-placement.yaml", "stack")
86 services = d.get_services()90 services = d.get_services()
87 self.assertEqual(services[0].name, 'nova-compute')91 self.assertEqual(services[0].name, 'nova-compute')
88 try:92 try:
@@ -164,7 +168,8 @@
164168
165 @skip_if_offline169 @skip_if_offline
166 def test_validate_invalid_placement_nested(self):170 def test_validate_invalid_placement_nested(self):
167 d = self.get_named_deployment_and_fetch_v3("stack-placement-invalid.yaml", "stack")171 d = self.get_named_deployment_and_fetch_v3(
172 "stack-placement-invalid.yaml", "stack")
168 services = d.get_services()173 services = d.get_services()
169 self.assertEqual(services[0].name, 'nova-compute')174 self.assertEqual(services[0].name, 'nova-compute')
170 try:175 try:
@@ -438,7 +443,8 @@
438 d.set_machines(machines)443 d.set_machines(machines)
439444
440 placement = d.get_unit_placement('mysql', status)445 placement = d.get_unit_placement('mysql', status)
441 self.assertEqual(placement.get_new_machines_for_containers(),446 self.assertEqual(
447 placement.get_new_machines_for_containers(),
442 ['mysql/0'])448 ['mysql/0'])
443 self.assertEqual(placement.get(0), 'lxc:2')449 self.assertEqual(placement.get(0), 'lxc:2')
444450
@@ -450,8 +456,8 @@
450 "nginx": {"consumes": ["wordpress"]}}}456 "nginx": {"consumes": ["wordpress"]}}}
451 d = Deployment("foo", data, include_dirs=())457 d = Deployment("foo", data, include_dirs=())
452 self.assertEqual(458 self.assertEqual(
453 [('nginx', 'wordpress'), ('wordpress', 'mysql')],459 sorted([('nginx', 'wordpress'), ('wordpress', 'mysql')]),
454 list(d.get_relations()))460 sorted(list(d.get_relations())))
455461
456 def test_multiple_relations_weighted(self):462 def test_multiple_relations_weighted(self):
457 data = {463 data = {
@@ -477,10 +483,12 @@
477483
478 def test_getting_service_names(self):484 def test_getting_service_names(self):
479 # It is possible to retrieve the service names.485 # It is possible to retrieve the service names.
480 deployment = self.get_named_deployment_v3("stack-placement.yaml", "stack")486 deployment = self.get_named_deployment_v3(
487 "stack-placement.yaml", "stack")
481 service_names = deployment.get_service_names()488 service_names = deployment.get_service_names()
482 expected_service_names = [489 expected_service_names = [
483 'ceph', 'mysql', 'nova-compute', 'quantum', 'semper', 'verity', 'lxc-service']490 'ceph', 'mysql', 'nova-compute', 'quantum',
491 'semper', 'verity', 'lxc-service']
484 self.assertEqual(set(expected_service_names), set(service_names))492 self.assertEqual(set(expected_service_names), set(service_names))
485493
486 def test_resolve_config_handles_empty_options(self):494 def test_resolve_config_handles_empty_options(self):
487495
=== modified file 'deployer/tests/test_diff.py'
--- deployer/tests/test_diff.py 2015-03-17 17:34:57 +0000
+++ deployer/tests/test_diff.py 2016-05-08 03:36:27 +0000
@@ -1,11 +1,13 @@
1""" Unittest for juju-deployer diff action (--diff) """1""" Unittest for juju-deployer diff action (--diff) """
2# pylint: disable=C01032# pylint: disable=C0103
3import StringIO3from __future__ import absolute_import
4import os4import os
5import shutil5import shutil
6import tempfile6import tempfile
7import unittest7import unittest
88
9from six import StringIO
10
9from deployer.config import ConfigStack11from deployer.config import ConfigStack
10from deployer.env.mem import MemoryEnvironment12from deployer.env.mem import MemoryEnvironment
11from deployer.utils import setup_logging13from deployer.utils import setup_logging
@@ -19,7 +21,7 @@
1921
20 def setUp(self):22 def setUp(self):
21 self.output = setup_logging(23 self.output = setup_logging(
22 debug=True, verbose=True, stream=StringIO.StringIO())24 debug=True, verbose=True, stream=StringIO())
2325
24 # Because fetch_charms is expensive, do it once for all tests26 # Because fetch_charms is expensive, do it once for all tests
25 @classmethod27 @classmethod
@@ -113,6 +115,7 @@
113 env = MemoryEnvironment(dpl.name, dpl)115 env = MemoryEnvironment(dpl.name, dpl)
114 env.destroy_service('haproxy')116 env.destroy_service('haproxy')
115 diff = Diff(env, dpl, {}).do_diff()117 diff = Diff(env, dpl, {}).do_diff()
116 self.assertTrue(str(diff['relations']['missing'][0]).find('haproxy')118 self.assertTrue(
117 != -1)119 str(diff['relations']['missing'][0]).find('haproxy') != -1)
118 self.assertTrue(diff['services']['missing'].keys() == ['haproxy'])120 self.assertTrue(
121 list(diff['services']['missing'].keys()) == ['haproxy'])
119122
=== modified file 'deployer/tests/test_goenv.py'
--- deployer/tests/test_goenv.py 2016-04-01 02:22:26 +0000
+++ deployer/tests/test_goenv.py 2016-05-08 03:36:27 +0000
@@ -1,3 +1,4 @@
1from __future__ import absolute_import
1import logging2import logging
2import os3import os
3import time4import time
@@ -44,7 +45,7 @@
44 self.assertFalse(status.get('services'))45 self.assertFalse(status.get('services'))
45 # Destroy everything.. consistent baseline46 # Destroy everything.. consistent baseline
46 self.env.reset(47 self.env.reset(
47 terminate_machines=len(status['machines'].keys()) > 1,48 terminate_machines=len(list(status['machines'].keys())) > 1,
48 terminate_delay=240)49 terminate_delay=240)
4950
50 def tearDown(self):51 def tearDown(self):
5152
=== modified file 'deployer/tests/test_guienv.py'
--- deployer/tests/test_guienv.py 2014-12-17 13:06:49 +0000
+++ deployer/tests/test_guienv.py 2016-05-08 03:36:27 +0000
@@ -1,5 +1,6 @@
1"""Tests for the GUIEnvironment."""1"""Tests for the GUIEnvironment."""
22
3from __future__ import absolute_import
3import unittest4import unittest
4import mock5import mock
56
67
=== modified file 'deployer/tests/test_guiserver.py'
--- deployer/tests/test_guiserver.py 2016-05-03 16:03:18 +0000
+++ deployer/tests/test_guiserver.py 2016-05-08 03:36:27 +0000
@@ -1,5 +1,6 @@
1"""Tests for the GUI server bundles deployment support."""1"""Tests for the GUI server bundles deployment support."""
22
3from __future__ import absolute_import
3from contextlib import contextmanager4from contextlib import contextmanager
4import os5import os
5import shutil6import shutil
67
=== modified file 'deployer/tests/test_importer.py'
--- deployer/tests/test_importer.py 2016-05-03 16:03:18 +0000
+++ deployer/tests/test_importer.py 2016-05-08 03:36:27 +0000
@@ -1,3 +1,4 @@
1from __future__ import absolute_import
1import os2import os
23
3import mock4import mock
@@ -5,7 +6,7 @@
5from deployer.config import ConfigStack6from deployer.config import ConfigStack
6from deployer.action.importer import Importer7from deployer.action.importer import Importer
78
8from base import (9from .base import (
9 Base,10 Base,
10 patch_env_status,11 patch_env_status,
11 skip_if_offline,12 skip_if_offline,
@@ -50,6 +51,7 @@
50 @skip_if_offline51 @skip_if_offline
51 @mock.patch('deployer.action.importer.time')52 @mock.patch('deployer.action.importer.time')
52 def test_importer(self, mock_time):53 def test_importer(self, mock_time):
54 mock_time.time.return_value = 0
53 # Trying to track down where this comes from http://pad.lv/124382755 # Trying to track down where this comes from http://pad.lv/1243827
54 stack = ConfigStack(self.options.configs)56 stack = ConfigStack(self.options.configs)
55 deploy = stack.get('wiki')57 deploy = stack.get('wiki')
@@ -69,6 +71,7 @@
69 @skip_if_offline71 @skip_if_offline
70 @mock.patch('deployer.action.importer.time')72 @mock.patch('deployer.action.importer.time')
71 def test_importer_no_relations(self, mock_time):73 def test_importer_no_relations(self, mock_time):
74 mock_time.time.return_value = 0
72 self.options.no_relations = True75 self.options.no_relations = True
73 stack = ConfigStack(self.options.configs)76 stack = ConfigStack(self.options.configs)
74 deploy = stack.get('wiki')77 deploy = stack.get('wiki')
@@ -81,6 +84,7 @@
81 @skip_if_offline84 @skip_if_offline
82 @mock.patch('deployer.action.importer.time')85 @mock.patch('deployer.action.importer.time')
83 def test_importer_add_machine_series(self, mock_time):86 def test_importer_add_machine_series(self, mock_time):
87 mock_time.time.return_value = 0
84 self.options.configs = [88 self.options.configs = [
85 os.path.join(self.test_data_dir, 'v4', 'series.yaml')]89 os.path.join(self.test_data_dir, 'v4', 'series.yaml')]
86 stack = ConfigStack(self.options.configs)90 stack = ConfigStack(self.options.configs)
@@ -91,16 +95,15 @@
91 importer.run()95 importer.run()
9296
93 self.assertEqual(env.add_machine.call_count, 2)97 self.assertEqual(env.add_machine.call_count, 2)
94 self.assertEqual(98 env.add_machine.assert_has_calls([
95 env.add_machine.call_args_list[0][1],99 mock.call(series='precise', constraints='mem=512M'),
96 {'series': 'precise', 'constraints': 'mem=512M'})100 mock.call(series='trusty', constraints='mem=512M'),
97 self.assertEqual(101 ], any_order=True)
98 env.add_machine.call_args_list[1][1],
99 {'series': 'trusty', 'constraints': 'mem=512M'})
100102
101 @skip_if_offline103 @skip_if_offline
102 @mock.patch('deployer.action.importer.time')104 @mock.patch('deployer.action.importer.time')
103 def test_importer_existing_machine(self, mock_time):105 def test_importer_existing_machine(self, mock_time):
106 mock_time.time.return_value = 0
104 self.options.configs = [107 self.options.configs = [
105 os.path.join(self.test_data_dir, 'v4',108 os.path.join(self.test_data_dir, 'v4',
106 'container-existing-machine.yaml')]109 'container-existing-machine.yaml')]
107110
=== modified file 'deployer/tests/test_pyenv.py'
--- deployer/tests/test_pyenv.py 2014-02-22 23:11:02 +0000
+++ deployer/tests/test_pyenv.py 2016-05-08 03:36:27 +0000
@@ -1,4 +1,6 @@
1import StringIO1from __future__ import absolute_import
2
3from six import StringIO
24
3from .base import Base5from .base import Base
4from deployer.env import watchers6from deployer.env import watchers
@@ -21,7 +23,7 @@
2123
22 def setUp(self):24 def setUp(self):
23 self.output = setup_logging(25 self.output = setup_logging(
24 debug=True, verbose=True, stream=StringIO.StringIO())26 debug=True, verbose=True, stream=StringIO())
2527
26 def test_wait_for_units_error_no_exit(self):28 def test_wait_for_units_error_no_exit(self):
27 env = FakePyEnvironment(29 env = FakePyEnvironment(
2830
=== modified file 'deployer/tests/test_service.py'
--- deployer/tests/test_service.py 2013-07-22 15:29:31 +0000
+++ deployer/tests/test_service.py 2016-05-08 03:36:27 +0000
@@ -1,3 +1,4 @@
1from __future__ import absolute_import
1from deployer.service import Service2from deployer.service import Service
2from .base import Base3from .base import Base
34
45
=== modified file 'deployer/tests/test_utils.py'
--- deployer/tests/test_utils.py 2016-01-11 13:35:47 +0000
+++ deployer/tests/test_utils.py 2016-05-08 03:36:27 +0000
@@ -1,5 +1,5 @@
1from __future__ import absolute_import
1import os2import os
2from StringIO import StringIO
3from subprocess import CalledProcessError3from subprocess import CalledProcessError
44
5from mock import (5from mock import (
@@ -7,6 +7,8 @@
7 patch,7 patch,
8)8)
99
10from six import BytesIO
11
10from .base import Base12from .base import Base
11from deployer.utils import (13from deployer.utils import (
12 _check_call,14 _check_call,
@@ -73,7 +75,7 @@
73 self.assertRaises(75 self.assertRaises(
74 OSError, _check_call, params=[cmd], log=MagicMock())76 OSError, _check_call, params=[cmd], log=MagicMock())
75 output = _check_call(params=[cmd], log=MagicMock(), shell=True)77 output = _check_call(params=[cmd], log=MagicMock(), shell=True)
76 self.assertEqual(output, "foo\n")78 self.assertEqual(output, b"foo\n")
7779
7880
79class TestMkdir(Base):81class TestMkdir(Base):
@@ -117,7 +119,7 @@
117 # Errors are correctly re-raised.119 # Errors are correctly re-raised.
118 path = os.path.join(self.playground, 'foo')120 path = os.path.join(self.playground, 'foo')
119 os.chmod(self.playground, 0000)121 os.chmod(self.playground, 0000)
120 self.addCleanup(os.chmod, self.playground, 0700)122 self.addCleanup(os.chmod, self.playground, 0o700)
121 with self.assertRaises(OSError):123 with self.assertRaises(OSError):
122 mkdir(os.path.join(path))124 mkdir(os.path.join(path))
123 self.assertFalse(os.path.exists(path))125 self.assertFalse(os.path.exists(path))
@@ -143,14 +145,14 @@
143 self.assertFalse(_is_qualified_charm_url(url))145 self.assertFalse(_is_qualified_charm_url(url))
144146
145 def test_get_qualified_url(self):147 def test_get_qualified_url(self):
146 fake_json = """148 fake_json = b"""
147 {"cs:precise/mysql":149 {"cs:precise/mysql":
148 {"revision":333}150 {"revision":333}
149 }151 }
150 """152 """
151153
152 def mocked_urlopen(url):154 def mocked_urlopen(url):
153 return StringIO(fake_json)155 return BytesIO(fake_json)
154156
155 path = 'deployer.utils.urlopen'157 path = 'deployer.utils.urlopen'
156 with patch(path, mocked_urlopen):158 with patch(path, mocked_urlopen):
@@ -164,10 +166,11 @@
164 with patch('deployer.utils.urlopen', mocked_urlopen):166 with patch('deployer.utils.urlopen', mocked_urlopen):
165 with self.assertRaises(DeploymentError) as exc:167 with self.assertRaises(DeploymentError) as exc:
166 get_qualified_charm_url('cs:precise/mysql')168 get_qualified_charm_url('cs:precise/mysql')
167 expected = ('HTTP Error 404: '169 expected = (
168 'Bad Earl (https://api.jujucharms.com/charmstore/charm-info'170 'HTTP Error 404: '
169 '?charms=cs:precise/mysql)')171 'Bad Earl (https://api.jujucharms.com/charmstore/charm-info'
170 self.assertEqual([expected], exc.exception.message)172 '?charms=cs:precise/mysql)')
173 self.assertEqual([expected], exc.exception.args[0])
171174
172 def test_get_qualified_url_raise_exception_on_URLError(self):175 def test_get_qualified_url_raise_exception_on_URLError(self):
173 def mocked_urlopen(url):176 def mocked_urlopen(url):
@@ -179,4 +182,4 @@
179 expected = ('<urlopen error Hinky URL> '182 expected = ('<urlopen error Hinky URL> '
180 '(https://api.jujucharms.com/charmstore/charm-info'183 '(https://api.jujucharms.com/charmstore/charm-info'
181 '?charms=cs:precise/mysql)')184 '?charms=cs:precise/mysql)')
182 self.assertEqual([expected], exc.exception.message)185 self.assertEqual([expected], exc.exception.args[0])
183186
=== modified file 'deployer/tests/test_watchers.py'
--- deployer/tests/test_watchers.py 2014-03-06 21:17:14 +0000
+++ deployer/tests/test_watchers.py 2016-05-08 03:36:27 +0000
@@ -1,5 +1,7 @@
1"""Tests juju-core environment watchers."""1"""Tests juju-core environment watchers."""
22
3from __future__ import absolute_import
4import operator
3import unittest5import unittest
46
5import mock7import mock
@@ -80,14 +82,21 @@
80 # The errors handler has been called once for each changeset containing82 # The errors handler has been called once for each changeset containing
81 # errors.83 # errors.
82 self.assertEqual(2, on_errors.call_count)84 self.assertEqual(2, on_errors.call_count)
83 on_errors.assert_has_calls([85 # Because of the implementation, we can't guarantee the order of the
84 mock.call([86 # list in the first call to on_errors here:
87 sort_key = operator.itemgetter('Name')
88 self.assertEqual(
89 sorted([
85 {'Status': 'error', 'Name': 'django/42', 'Service': 'django'},90 {'Status': 'error', 'Name': 'django/42', 'Service': 'django'},
86 {'Status': 'error', 'Name': 'haproxy/1', 'Service': 'haproxy'}91 {'Status': 'error', 'Name': 'haproxy/1', 'Service': 'haproxy'}
87 ]),92 ], key=sort_key),
88 mock.call([93 # [0][0][0] = first call, positional args, first positional arg
89 {'Status': 'error', 'Name': 'django/0', 'Service': 'django'}]),94 sorted(on_errors.call_args_list[0][0][0], key=sort_key)
90 ])95 )
96 self.assertEqual(
97 [{'Status': 'error', 'Name': 'django/0', 'Service': 'django'}],
98 on_errors.call_args_list[1][0][0]
99 )
91100
92 def test_specific_services(self):101 def test_specific_services(self):
93 # It is possible to only watch units belonging to specific services.102 # It is possible to only watch units belonging to specific services.
@@ -164,4 +173,4 @@
164 callback = watchers.raise_on_errors(ValueError)173 callback = watchers.raise_on_errors(ValueError)
165 with self.assertRaises(ValueError) as cm:174 with self.assertRaises(ValueError) as cm:
166 callback('bad wolf')175 callback('bad wolf')
167 self.assertEqual('bad wolf', bytes(cm.exception))176 self.assertEqual('bad wolf', cm.exception.args[0])
168177
=== modified file 'deployer/utils.py'
--- deployer/utils.py 2016-05-03 17:28:13 +0000
+++ deployer/utils.py 2016-05-08 03:36:27 +0000
@@ -1,3 +1,4 @@
1from __future__ import absolute_import
1from copy import deepcopy2from copy import deepcopy
2from contextlib import contextmanager3from contextlib import contextmanager
34
@@ -20,12 +21,15 @@
20import subprocess21import subprocess
21import time22import time
22import tempfile23import tempfile
23from urllib2 import (24from six.moves.urllib.error import (
24 HTTPError,25 HTTPError,
25 URLError,26 URLError,
27)
28from six.moves.urllib.request import (
26 urlopen,29 urlopen,
27)30)
28import zipfile31import zipfile
32import six
2933
30try:34try:
31 from yaml import CSafeLoader, CSafeDumper35 from yaml import CSafeLoader, CSafeDumper
@@ -71,7 +75,7 @@
71 node = yaml.ScalarNode(tag=u'tag:yaml.org,2002:str', value=uni)75 node = yaml.ScalarNode(tag=u'tag:yaml.org,2002:str', value=uni)
72 return node76 return node
7377
74yaml.add_representer(unicode, _unicode_representer)78yaml.add_representer(six.text_type, _unicode_representer)
7579
7680
77DEFAULT_LOGGING = """81DEFAULT_LOGGING = """
@@ -250,12 +254,14 @@
250254
251255
252_juju_major_version = None256_juju_major_version = None
257
258
253def get_juju_major_version():259def get_juju_major_version():
254 global _juju_major_version260 global _juju_major_version
255 if _juju_major_version is None:261 if _juju_major_version is None:
256 log = logging.getLogger("deployer.utils")262 log = logging.getLogger("deployer.utils")
257 _juju_major_version = int(_check_call(263 _juju_major_version = int(_check_call(
258 ["juju", "--version"], log).split('.')[0])264 ["juju", "--version"], log).split(b'.')[0])
259 return _juju_major_version265 return _juju_major_version
260266
261267
@@ -370,7 +376,7 @@
370 except (HTTPError, URLError) as e:376 except (HTTPError, URLError) as e:
371 errmsg = '{} ({})'.format(e, info_url)377 errmsg = '{} ({})'.format(e, info_url)
372 raise DeploymentError([errmsg])378 raise DeploymentError([errmsg])
373 content = json.loads(fh.read())379 content = json.loads(fh.read().decode())
374 rev = content[url]['revision']380 rev = content[url]['revision']
375 return "%s-%d" % (url, rev)381 return "%s-%d" % (url, rev)
376382
@@ -399,7 +405,7 @@
399 else:405 else:
400 # Return the juju2 controller:model combo406 # Return the juju2 controller:model combo
401 log = logging.getLogger("deployer.utils")407 log = logging.getLogger("deployer.utils")
402 return _check_call(["juju", "switch"], log).strip()408 return _check_call(["juju", "switch"], log).strip().decode()
403409
404410
405def x_in_y(x, y):411def x_in_y(x, y):
406412
=== modified file 'deployer/vcs.py'
--- deployer/vcs.py 2016-03-03 14:18:30 +0000
+++ deployer/vcs.py 2016-05-08 03:36:27 +0000
@@ -1,9 +1,8 @@
1from __future__ import absolute_import
1import subprocess2import subprocess
2import os3import os
3import re4import re
45
5from bzrlib.workingtree import WorkingTree
6
7from .utils import ErrorExit6from .utils import ErrorExit
87
98
@@ -34,10 +33,10 @@
34 stderr = subprocess.STDOUT33 stderr = subprocess.STDOUT
35 output = subprocess.check_output(34 output = subprocess.check_output(
36 args, cwd=cwd or self.path, stderr=stderr)35 args, cwd=cwd or self.path, stderr=stderr)
37 except subprocess.CalledProcessError, e:36 except subprocess.CalledProcessError as e:
38 self.log.error(error_msg % self.get_err_msg_ctx(e))37 self.log.error(error_msg % self.get_err_msg_ctx(e))
39 raise ErrorExit()38 raise ErrorExit()
40 return output.strip()39 return output.strip().decode()
4140
42 def get_err_msg_ctx(self, e):41 def get_err_msg_ctx(self, e):
43 return {42 return {
@@ -99,10 +98,9 @@
99 self._call(params, self.err_branch, cwd)98 self._call(params, self.err_branch, cwd)
10099
101 def is_modified(self):100 def is_modified(self):
102 # To replace with bzr cli, we need to be able to detect101 return subprocess.call(
103 # changes to a wc @ a rev or @ trunk.102 ["bzr", "diff"],
104 tree = WorkingTree.open(self.path)103 cwd=self.path, stdout=subprocess.PIPE) != 0
105 return tree.has_changes()
106104
107105
108class Git(Vcs):106class Git(Vcs):
109107
=== modified file 'doc/conf.py'
--- doc/conf.py 2013-05-16 03:05:55 +0000
+++ doc/conf.py 2016-05-08 03:36:27 +0000
@@ -11,6 +11,7 @@
11# All configuration values have a default; values that are commented out11# All configuration values have a default; values that are commented out
12# serve to show the default.12# serve to show the default.
1313
14from __future__ import absolute_import
14import sys, os15import sys, os
1516
16# If extensions (or modules to document with autodoc) are in another directory,17# If extensions (or modules to document with autodoc) are in another directory,
1718
=== modified file 'setup.py'
--- setup.py 2016-05-05 17:42:42 +0000
+++ setup.py 2016-05-08 03:36:27 +0000
@@ -1,3 +1,4 @@
1from __future__ import absolute_import
1from setuptools import setup, find_packages2from setuptools import setup, find_packages
23
34
@@ -9,11 +10,15 @@
9 author="Kapil Thangavelu",10 author="Kapil Thangavelu",
10 author_email="kapil.foss@gmail.com",11 author_email="kapil.foss@gmail.com",
11 url="http://launchpad.net/juju-deployer",12 url="http://launchpad.net/juju-deployer",
12 install_requires=["jujuclient>=0.18", "PyYAML>=3.10", "bzr"],13 install_requires=["jujuclient>=0.18", "PyYAML>=3.10", "six"],
13 packages=find_packages(),14 packages=find_packages(),
14 classifiers=[15 classifiers=[
15 "Development Status :: 4 - Beta",16 "Development Status :: 4 - Beta",
16 "Programming Language :: Python",17 "Programming Language :: Python",
18 "Programming Language :: Python :: 2",
19 "Programming Language :: Python :: 2.7",
20 "Programming Language :: Python :: 3",
21 "Programming Language :: Python :: 3.5",
17 "Topic :: Internet",22 "Topic :: Internet",
18 "Topic :: Software Development :: Libraries :: Python Modules",23 "Topic :: Software Development :: Libraries :: Python Modules",
19 "Intended Audience :: System Administrators",24 "Intended Audience :: System Administrators",
2025
=== added file 'test-requirements.txt'
--- test-requirements.txt 1970-01-01 00:00:00 +0000
+++ test-requirements.txt 2016-05-08 03:36:27 +0000
@@ -0,0 +1,4 @@
1coverage
2flake8
3mock
4nose
05
=== added file 'tox.ini'
--- tox.ini 1970-01-01 00:00:00 +0000
+++ tox.ini 2016-05-08 03:36:27 +0000
@@ -0,0 +1,20 @@
1[tox]
2minversion = 1.8
3envlist = py27,py35,pep8
4
5[testenv]
6usedevelop=True
7
8# need SSH_AUTH_SOCK for bzr calls to work
9passenv = SSH_AUTH_SOCK
10
11deps = -r{toxinidir}/test-requirements.txt
12setenv =
13 JUJU_TEST_ENV = {env:JUJU_TEST_ENV:"test"}
14 JUJU_DATA = {homedir}/.local/share/juju
15 HOME = {env:HOME}
16commands=
17 nosetests --with-coverage --cover-package=deployer deployer/tests
18
19[testenv:pep8]
20commands = flake8 deployer

Subscribers

People subscribed via source and target branches