Merge lp:~johnsca/charm-helpers/py3-bzr into lp:charm-helpers

Proposed by Cory Johns
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
Reviewer Review Type Date Requested Status
Stuart Bishop (community) Approve
Review via email: mp+280088@code.launchpad.net

Description of the change

Python 3 support by using CLI bzr instead of bzrlib

To post a comment you must log in.
Revision history for this message
Stuart Bishop (stub) wrote :

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.

review: Needs Fixing
Revision history for this message
Cory Johns (johnsca) wrote :

You're right about it being an unexpected backwards-incompatible change. I just loathe the hidden install-on-demand behavior so much. :p

Thanks for the other comments. I will address them and update.

lp:~johnsca/charm-helpers/py3-bzr updated
505. By Cory Johns

Merged upstream changes

506. By Cory Johns

Fixes from review

507. By Cory Johns

Fixed git & bzr install failure handling

Revision history for this message
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.

Revision history for this message
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_install(['python-git'])
   try:
       import git
   except ImportError:
       raise NotImplemetedError()
else:
   raise NotImplementedError()

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.

review: Approve
lp:~johnsca/charm-helpers/py3-bzr updated
508. By Cory Johns

Merged upstream changes

509. By Cory Johns

Converted giturl to use CLI and be idempotent

Revision history for this message
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`?).

Revision history for this message
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.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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()

Subscribers

People subscribed via source and target branches