Merge lp:~johnsca/charm-helpers/py3-bzr into lp:charm-helpers
- py3-bzr
- Merge into devel
Status: | Merged |
---|---|
Merged at revision: | 507 |
Proposed branch: | lp:~johnsca/charm-helpers/py3-bzr |
Merge into: | lp:charm-helpers |
Diff against target: |
385 lines (+91/-77) 7 files modified
charmhelpers/fetch/__init__.py (+1/-1) charmhelpers/fetch/bzrurl.py (+15/-29) charmhelpers/fetch/giturl.py (+18/-16) test_requirements.txt (+0/-1) tests/fetch/test_bzrurl.py (+24/-14) tests/fetch/test_fetch.py (+2/-6) tests/fetch/test_giturl.py (+31/-10) |
To merge this branch: | bzr merge lp:~johnsca/charm-helpers/py3-bzr |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stuart Bishop (community) | Approve | ||
Review via email: mp+280088@code.launchpad.net |
Commit message
Description of the change
Python 3 support by using CLI bzr instead of bzrlib
Cory Johns (johnsca) wrote : | # |
You're right about it being an unexpected backwards-
Thanks for the other comments. I will address them and update.
- 505. By Cory Johns
-
Merged upstream changes
- 506. By Cory Johns
-
Fixes from review
- 507. By Cory Johns
-
Fixed git & bzr install failure handling
Cory Johns (johnsca) wrote : | # |
I went with a simpler exists check for the idempotency even though it's not exactly equivalent to what you suggested. I trust it is still sufficient?
Also, I had to add support for local repos to get your suggested test case to work, but I think it's worth it, even if it's unlikely to be used in practice.
Stuart Bishop (stub) wrote : | # |
All good, except for the git package bootstrap. Unlike 'apt-get install git', 'pip install GitPython' will not pull in the git(1) command line tool. It will also fail in environments without egress to pypi.
Humph. And python3-git doesn't exist yet (I just filed a bug to get the package built in Xenial).
So unless we want to say screw it and use command line git rather than a Python wrapper, the same as we are doing with bzr, then the best I can come up with is:
if six.PY2:
apt_
try:
import git
except ImportError:
raise NotImplemetedEr
else:
raise NotImplementedE
ie. keep auto-install of the python-git package for Python2, and make it the charmers problem if they are using Python3. This is still an improvement, since at the moment Python3 charms do not have access to this API at all.
This can land if you revert the Python2 behaviour to the apt install, so giving it my conditional approval.
- 508. By Cory Johns
-
Merged upstream changes
- 509. By Cory Johns
-
Converted giturl to use CLI and be idempotent
Cory Johns (johnsca) wrote : | # |
I went ahead and converted git to use the CLI. Makes it cleaner and more consistent. I also fixed the idempotency issue for git. Previously, it would fail if called twice; now it works somewhat similarly to the bzr (though git doesn't have an analogue to --overwrite; maybe I should have just `rm -rf dest`?).
Stuart Bishop (stub) wrote : | # |
All looks good to me.
If you have messed up your branches enough that the --overwrite is actually needed, there is some other problem. I just put it in my example for robustness. I wouldn't do the rm -rf, as that would suck if the source branch is huge.
Preview Diff
1 | === modified file 'charmhelpers/fetch/__init__.py' |
2 | --- charmhelpers/fetch/__init__.py 2015-10-26 09:47:10 +0000 |
3 | +++ charmhelpers/fetch/__init__.py 2015-12-11 17:02:29 +0000 |
4 | @@ -411,7 +411,7 @@ |
5 | importlib.import_module(package), |
6 | classname) |
7 | plugin_list.append(handler_class()) |
8 | - except (ImportError, AttributeError): |
9 | + except NotImplementedError: |
10 | # Skip missing plugins so that they can be ommitted from |
11 | # installation if desired |
12 | log("FetchHandler {} not found, skipping plugin".format( |
13 | |
14 | === modified file 'charmhelpers/fetch/bzrurl.py' |
15 | --- charmhelpers/fetch/bzrurl.py 2015-11-22 21:31:56 +0000 |
16 | +++ charmhelpers/fetch/bzrurl.py 2015-12-11 17:02:29 +0000 |
17 | @@ -15,54 +15,40 @@ |
18 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
19 | |
20 | import os |
21 | +from subprocess import check_call |
22 | from charmhelpers.fetch import ( |
23 | BaseFetchHandler, |
24 | - UnhandledSource |
25 | + UnhandledSource, |
26 | + filter_installed_packages, |
27 | + apt_install, |
28 | ) |
29 | from charmhelpers.core.host import mkdir |
30 | |
31 | -import six |
32 | -if six.PY3: |
33 | - raise ImportError('bzrlib does not support Python3') |
34 | |
35 | -try: |
36 | - from bzrlib.branch import Branch |
37 | - from bzrlib import bzrdir, workingtree, errors |
38 | -except ImportError: |
39 | - from charmhelpers.fetch import apt_install |
40 | - apt_install("python-bzrlib") |
41 | - from bzrlib.branch import Branch |
42 | - from bzrlib import bzrdir, workingtree, errors |
43 | +if filter_installed_packages(['bzr']) != []: |
44 | + apt_install(['bzr']) |
45 | + if filter_installed_packages(['bzr']) != []: |
46 | + raise NotImplementedError('Unable to install bzr') |
47 | |
48 | |
49 | class BzrUrlFetchHandler(BaseFetchHandler): |
50 | """Handler for bazaar branches via generic and lp URLs""" |
51 | def can_handle(self, source): |
52 | url_parts = self.parse_url(source) |
53 | - if url_parts.scheme not in ('bzr+ssh', 'lp'): |
54 | + if url_parts.scheme not in ('bzr+ssh', 'lp', ''): |
55 | return False |
56 | + elif not url_parts.scheme: |
57 | + return os.path.exists(os.path.join(source, '.bzr')) |
58 | else: |
59 | return True |
60 | |
61 | def branch(self, source, dest): |
62 | - url_parts = self.parse_url(source) |
63 | - # If we use lp:branchname scheme we need to load plugins |
64 | if not self.can_handle(source): |
65 | raise UnhandledSource("Cannot handle {}".format(source)) |
66 | - if url_parts.scheme == "lp": |
67 | - from bzrlib.plugin import load_plugins |
68 | - load_plugins() |
69 | - try: |
70 | - local_branch = bzrdir.BzrDir.create_branch_convenience(dest) |
71 | - except errors.AlreadyControlDirError: |
72 | - local_branch = Branch.open(dest) |
73 | - try: |
74 | - remote_branch = Branch.open(source) |
75 | - remote_branch.push(local_branch) |
76 | - tree = workingtree.WorkingTree.open(dest) |
77 | - tree.update() |
78 | - except Exception as e: |
79 | - raise e |
80 | + if os.path.exists(dest): |
81 | + check_call(['bzr', 'pull', '--overwrite', '-d', dest, source]) |
82 | + else: |
83 | + check_call(['bzr', 'branch', source, dest]) |
84 | |
85 | def install(self, source, dest=None): |
86 | url_parts = self.parse_url(source) |
87 | |
88 | === modified file 'charmhelpers/fetch/giturl.py' |
89 | --- charmhelpers/fetch/giturl.py 2015-12-07 20:45:58 +0000 |
90 | +++ charmhelpers/fetch/giturl.py 2015-12-11 17:02:29 +0000 |
91 | @@ -15,20 +15,19 @@ |
92 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
93 | |
94 | import os |
95 | +from subprocess import check_call |
96 | from charmhelpers.fetch import ( |
97 | BaseFetchHandler, |
98 | - UnhandledSource |
99 | + UnhandledSource, |
100 | + filter_installed_packages, |
101 | + apt_install, |
102 | ) |
103 | from charmhelpers.core.host import mkdir |
104 | |
105 | -try: |
106 | - from git import Repo |
107 | -except ImportError: |
108 | - from charmhelpers.fetch import apt_install |
109 | - apt_install("python-git") |
110 | - from git import Repo |
111 | - |
112 | -from git.exc import GitCommandError # noqa E402 |
113 | +if filter_installed_packages(['git']) != []: |
114 | + apt_install(['git']) |
115 | + if filter_installed_packages(['git']) != []: |
116 | + raise NotImplementedError('Unable to install git') |
117 | |
118 | |
119 | class GitUrlFetchHandler(BaseFetchHandler): |
120 | @@ -36,19 +35,24 @@ |
121 | def can_handle(self, source): |
122 | url_parts = self.parse_url(source) |
123 | # TODO (mattyw) no support for ssh git@ yet |
124 | - if url_parts.scheme not in ('http', 'https', 'git'): |
125 | + if url_parts.scheme not in ('http', 'https', 'git', ''): |
126 | return False |
127 | + elif not url_parts.scheme: |
128 | + return os.path.exists(os.path.join(source, '.git')) |
129 | else: |
130 | return True |
131 | |
132 | - def clone(self, source, dest, branch, depth=None): |
133 | + def clone(self, source, dest, branch="master", depth=None): |
134 | if not self.can_handle(source): |
135 | raise UnhandledSource("Cannot handle {}".format(source)) |
136 | |
137 | + if os.path.exists(dest): |
138 | + cmd = ['git', '-C', dest, 'pull', source, branch] |
139 | + else: |
140 | + cmd = ['git', 'clone', source, dest, '--branch', branch] |
141 | if depth: |
142 | - Repo.clone_from(source, dest, branch=branch, depth=depth) |
143 | - else: |
144 | - Repo.clone_from(source, dest, branch=branch) |
145 | + cmd.extend(['--depth', depth]) |
146 | + check_call(cmd) |
147 | |
148 | def install(self, source, branch="master", dest=None, depth=None): |
149 | url_parts = self.parse_url(source) |
150 | @@ -62,8 +66,6 @@ |
151 | mkdir(dest_dir, perms=0o755) |
152 | try: |
153 | self.clone(source, dest_dir, branch, depth) |
154 | - except GitCommandError as e: |
155 | - raise UnhandledSource(e) |
156 | except OSError as e: |
157 | raise UnhandledSource(e.strerror) |
158 | return dest_dir |
159 | |
160 | === modified file 'test_requirements.txt' |
161 | --- test_requirements.txt 2015-12-08 05:07:07 +0000 |
162 | +++ test_requirements.txt 2015-12-11 17:02:29 +0000 |
163 | @@ -17,4 +17,3 @@ |
164 | netifaces==0.10 # trusty is 0.8, but using py3 compatible version for tests. |
165 | Jinja2==2.6 # precise |
166 | six==1.1 # precise |
167 | -GitPython>=0.3.4 # trusty is ~0.3.2, but using py3 compatible version for tests. |
168 | |
169 | === modified file 'tests/fetch/test_bzrurl.py' |
170 | --- tests/fetch/test_bzrurl.py 2015-01-14 14:48:12 +0000 |
171 | +++ tests/fetch/test_bzrurl.py 2015-12-11 17:02:29 +0000 |
172 | @@ -1,10 +1,12 @@ |
173 | import os |
174 | +import shutil |
175 | +import subprocess |
176 | +import tempfile |
177 | from testtools import TestCase |
178 | from mock import ( |
179 | MagicMock, |
180 | patch, |
181 | ) |
182 | -import unittest |
183 | |
184 | import six |
185 | if six.PY3: |
186 | @@ -22,13 +24,10 @@ |
187 | UnhandledSource = None |
188 | |
189 | |
190 | -@unittest.skipIf(six.PY3, 'bzr does not support Python 3') |
191 | class BzrUrlFetchHandlerTest(TestCase): |
192 | |
193 | def setUp(self): |
194 | super(BzrUrlFetchHandlerTest, self).setUp() |
195 | - if six.PY3: |
196 | - return |
197 | self.valid_urls = ( |
198 | "bzr+ssh://example.com/branch-name", |
199 | "bzr+ssh://example.com/branch-name/", |
200 | @@ -55,7 +54,6 @@ |
201 | ) |
202 | self.fh = bzrurl.BzrUrlFetchHandler() |
203 | |
204 | - @unittest.skipIf(six.PY3, 'bzr does not support Python 3') |
205 | def test_handles_bzr_urls(self): |
206 | for url in self.valid_urls: |
207 | result = self.fh.can_handle(url) |
208 | @@ -64,26 +62,38 @@ |
209 | result = self.fh.can_handle(url) |
210 | self.assertNotEqual(result, True, url) |
211 | |
212 | - @unittest.skipIf(six.PY3, 'bzr does not support Python 3') |
213 | - @patch('bzrlib.workingtree.WorkingTree.open') |
214 | - @patch('bzrlib.bzrdir.BzrDir.create_branch_convenience') |
215 | - @patch('bzrlib.branch.Branch.open') |
216 | - def test_branch(self, _open, _bzrdir, _tree_open): |
217 | + @patch('charmhelpers.fetch.bzrurl.check_call') |
218 | + def test_branch(self, check_call): |
219 | dest_path = "/destination/path" |
220 | for url in self.valid_urls: |
221 | self.fh.remote_branch = MagicMock() |
222 | self.fh.load_plugins = MagicMock() |
223 | self.fh.branch(url, dest_path) |
224 | |
225 | - _open.assert_called_with(url) |
226 | - _bzrdir.assert_called_with(dest_path) |
227 | - _tree_open.assert_called_with(dest_path) |
228 | + check_call.assert_called_with(['bzr', 'branch', url, dest_path]) |
229 | |
230 | for url in self.invalid_urls: |
231 | with patch.dict('os.environ', {'CHARM_DIR': 'foo'}): |
232 | self.assertRaises(UnhandledSource, self.fh.branch, url, dest_path) |
233 | |
234 | - @unittest.skipIf(six.PY3, 'bzr does not support Python 3') |
235 | + def test_branch_functional(self): |
236 | + src = None |
237 | + dst = None |
238 | + try: |
239 | + src = tempfile.mkdtemp() |
240 | + subprocess.check_call(['bzr', 'init', src]) |
241 | + dst = tempfile.mkdtemp() |
242 | + os.rmdir(dst) |
243 | + self.fh.branch(src, dst) |
244 | + assert os.path.exists(os.path.join(dst, '.bzr')) |
245 | + self.fh.branch(src, dst) # idempotent |
246 | + assert os.path.exists(os.path.join(dst, '.bzr')) |
247 | + finally: |
248 | + if src: |
249 | + shutil.rmtree(src, ignore_errors=True) |
250 | + if dst: |
251 | + shutil.rmtree(dst, ignore_errors=True) |
252 | + |
253 | @patch('charmhelpers.fetch.bzrurl.mkdir') |
254 | def test_installs(self, _mkdir): |
255 | self.fh.branch = MagicMock() |
256 | |
257 | === modified file 'tests/fetch/test_fetch.py' |
258 | --- tests/fetch/test_fetch.py 2015-12-08 05:07:07 +0000 |
259 | +++ tests/fetch/test_fetch.py 2015-12-11 17:02:29 +0000 |
260 | @@ -403,7 +403,7 @@ |
261 | @patch('charmhelpers.fetch.importlib.import_module') |
262 | def test_skips_and_logs_missing_plugins(self, import_, log_): |
263 | fetch_handlers = ['a.foo', 'b.foo', 'c.foo'] |
264 | - import_.side_effect = (ImportError, AttributeError, MagicMock()) |
265 | + import_.side_effect = (NotImplementedError, NotImplementedError, MagicMock()) |
266 | plugins = fetch.plugins(fetch_handlers) |
267 | |
268 | self.assertEqual(1, len(plugins)) |
269 | @@ -412,11 +412,7 @@ |
270 | @patch('charmhelpers.fetch.log') |
271 | def test_plugins_are_valid(self, log_): |
272 | plugins = fetch.plugins() |
273 | - if not six.PY3: |
274 | - self.assertEqual(len(fetch.FETCH_HANDLERS), len(plugins)) |
275 | - else: |
276 | - # No bzr libraries for Python3. |
277 | - self.assertEqual(len(fetch.FETCH_HANDLERS) - 1, len(plugins)) |
278 | + self.assertEqual(len(fetch.FETCH_HANDLERS), len(plugins)) |
279 | |
280 | |
281 | class BaseFetchHandlerTest(TestCase): |
282 | |
283 | === modified file 'tests/fetch/test_giturl.py' |
284 | --- tests/fetch/test_giturl.py 2015-12-08 05:07:07 +0000 |
285 | +++ tests/fetch/test_giturl.py 2015-12-11 17:02:29 +0000 |
286 | @@ -1,10 +1,12 @@ |
287 | import os |
288 | +import shutil |
289 | +import subprocess |
290 | +import tempfile |
291 | from testtools import TestCase |
292 | from mock import ( |
293 | MagicMock, |
294 | patch, |
295 | ) |
296 | -import unittest |
297 | |
298 | import six |
299 | if six.PY3: |
300 | @@ -12,6 +14,7 @@ |
301 | else: |
302 | from urlparse import urlparse |
303 | |
304 | +from charmhelpers.core.host import chdir |
305 | try: |
306 | from charmhelpers.fetch import ( |
307 | giturl, |
308 | @@ -23,8 +26,6 @@ |
309 | |
310 | |
311 | class GitUrlFetchHandlerTest(TestCase): |
312 | - |
313 | - @unittest.skipIf(six.PY3, 'git does not support Python 3') |
314 | def setUp(self): |
315 | super(GitUrlFetchHandlerTest, self).setUp() |
316 | self.valid_urls = ( |
317 | @@ -39,7 +40,6 @@ |
318 | ) |
319 | self.fh = giturl.GitUrlFetchHandler() |
320 | |
321 | - @unittest.skipIf(six.PY3, 'git does not support Python 3') |
322 | def test_handles_git_urls(self): |
323 | for url in self.valid_urls: |
324 | result = self.fh.can_handle(url) |
325 | @@ -48,9 +48,8 @@ |
326 | result = self.fh.can_handle(url) |
327 | self.assertNotEqual(result, True, url) |
328 | |
329 | - @unittest.skipIf(six.PY3, 'git does not support Python 3') |
330 | - @patch('git.Repo.clone_from') |
331 | - def test_branch(self, _clone_from): |
332 | + @patch.object(giturl, 'check_call') |
333 | + def test_clone(self, check_call): |
334 | dest_path = "/destination/path" |
335 | branch = "master" |
336 | for url in self.valid_urls: |
337 | @@ -58,7 +57,7 @@ |
338 | self.fh.load_plugins = MagicMock() |
339 | self.fh.clone(url, dest_path, branch, None) |
340 | |
341 | - _clone_from.assert_called_with(url, dest_path, branch=branch) |
342 | + check_call.assert_called_with(['git', 'clone', url, dest_path, '--branch', branch]) |
343 | |
344 | for url in self.invalid_urls: |
345 | with patch.dict('os.environ', {'CHARM_DIR': 'foo'}): |
346 | @@ -66,7 +65,30 @@ |
347 | dest_path, None, |
348 | branch) |
349 | |
350 | - @unittest.skipIf(six.PY3, 'git does not support Python 3') |
351 | + def test_clone_functional(self): |
352 | + src = None |
353 | + dst = None |
354 | + try: |
355 | + src = tempfile.mkdtemp() |
356 | + with chdir(src): |
357 | + subprocess.check_call(['git', 'init']) |
358 | + subprocess.check_call(['git', 'config', 'user.name', 'Joe']) |
359 | + subprocess.check_call(['git', 'config', 'user.email', 'joe@test.com']) |
360 | + subprocess.check_call(['touch', 'foo']) |
361 | + subprocess.check_call(['git', 'add', 'foo']) |
362 | + subprocess.check_call(['git', 'commit', '-m', 'test']) |
363 | + dst = tempfile.mkdtemp() |
364 | + os.rmdir(dst) |
365 | + self.fh.clone(src, dst) |
366 | + assert os.path.exists(os.path.join(dst, '.git')) |
367 | + self.fh.clone(src, dst) # idempotent |
368 | + assert os.path.exists(os.path.join(dst, '.git')) |
369 | + finally: |
370 | + if src: |
371 | + shutil.rmtree(src, ignore_errors=True) |
372 | + if dst: |
373 | + shutil.rmtree(dst, ignore_errors=True) |
374 | + |
375 | @patch('charmhelpers.fetch.giturl.mkdir') |
376 | def test_installs(self, _mkdir): |
377 | self.fh.clone = MagicMock() |
378 | @@ -80,7 +102,6 @@ |
379 | self.assertEqual(where, dest) |
380 | _mkdir.assert_called_with(where, perms=0o755) |
381 | |
382 | - @unittest.skipIf(six.PY3, 'git does not support Python 3') |
383 | @patch('charmhelpers.fetch.giturl.mkdir') |
384 | def test_installs_specified_dest(self, _mkdir): |
385 | self.fh.clone = MagicMock() |
Comments in line.
I think we need to keep the automagic package installation for now, as charm authors should not be responsible for bootstrapping the libraries that may be required by charmhelpers.fetch depending on the configuration set by the charm user. This will become a noop for reactive charms once it has a better way of bootstrapping architecture dependent packages (better than this ImportError hack at least).
The branch method needs to be idempotent. Simple fix described inline.
The existing tests seem rather barren. Again, alternatives described inline.