Merge lp:~gandelman-a/charm-helpers/sync_include_hints into lp:charm-helpers

Proposed by Adam Gandelman
Status: Merged
Merged at revision: 68
Proposed branch: lp:~gandelman-a/charm-helpers/sync_include_hints
Merge into: lp:charm-helpers
Diff against target: 347 lines (+166/-32)
4 files modified
setup.cfg (+1/-1)
tests/tools/test_charm_helper_sync.py (+63/-11)
tools/charm_helpers_sync/README (+25/-0)
tools/charm_helpers_sync/charm_helpers_sync.py (+77/-20)
To merge this branch: bzr merge lp:~gandelman-a/charm-helpers/sync_include_hints
Reviewer Review Type Date Requested Status
James Page Approve
Adam Gandelman (community) Needs Resubmitting
Review via email: mp+174320@code.launchpad.net

Commit message

charm_helpers_sync: Allows adding hints to charm sync config to selectively include non-.py files.

Description of the change

We'll (hopefully) soon be including a collection of common template files with the OpenStack helpers. I'd like to be able to sync these into my charms as part of my current workflow, using the charm_helpers_sync tool. This updates the script to allow passing additional config to sync sources, to include files that would currently be excluded from the sync.

To post a comment you must log in.
Revision history for this message
James Page (james-page) wrote :

I think this looks OK - but I did notice that the tests for this tool are not running by default AFAICT.

Running:

python /usr/bin/nosetests --nologcapture tests/tools

Generates alot of output right now but the tests do pass.

review: Needs Information
49. By Adam Gandelman

setup.cfg: Include tools/ in coverage

Revision history for this message
Adam Gandelman (gandelman-a) wrote :

Hmm. Looks like they are running by default:

nosetests -v tests/
...
tests.payload.test_execd.ExecDTestCase.test_execd_submodule_list ... ok
It properly branches the correct helpers branch ... ok
It correctly finds the correct install path within a charm ... ok
It ensures all subdirectories of a parent are python importable ... ok
It extracts multiple options from an included item ... ok
...
Ran 324 tests in 3.461s (with tests/tools/)

vs.

Ran 306 tests in 1.600s (after removing tests/tools)

However, tools/ was not being included in the coverage report, though. I've updated setup.cfg to include it.

Revision history for this message
Adam Gandelman (gandelman-a) :
review: Needs Resubmitting
Revision history for this message
James Page (james-page) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'setup.cfg'
2--- setup.cfg 2013-05-11 20:24:29 +0000
3+++ setup.cfg 2013-07-16 21:31:23 +0000
4@@ -1,4 +1,4 @@
5 [nosetests]
6 with-coverage=1
7 cover-erase=1
8-cover-package=charmhelpers
9+cover-package=charmhelpers,tools
10
11=== modified file 'tests/tools/test_charm_helper_sync.py'
12--- tests/tools/test_charm_helper_sync.py 2013-06-27 22:02:19 +0000
13+++ tests/tools/test_charm_helper_sync.py 2013-07-16 21:31:23 +0000
14@@ -87,10 +87,8 @@
15 self.assertIn(copy_i, copy.call_args_list)
16 ensure_init.assert_called_with('hooks/charmhelpers/core')
17
18- @patch('os.path.isdir')
19- @patch('os.path.isfile')
20- def test_filter_dir(self, isfile, isdir):
21- '''It filters non-python files and non-module dirs from sync source'''
22+ def _test_filter_dir(self, opts, isfile, isdir):
23+ '''It filters non-python files and non-module dirs from source'''
24 files = {
25 'bad_file.bin': 'f',
26 'some_dir': 'd',
27@@ -114,12 +112,34 @@
28
29 isfile.side_effect = _isfile
30 isdir.side_effect = _isdir
31- result = sync._filter(dir='/tmp/charm-helpers/core',
32- ls=files.iterkeys())
33+ result = sync.get_filter(opts)(dir='/tmp/charm-helpers/core',
34+ ls=files.iterkeys())
35+ return result
36+
37+ @patch('os.path.isdir')
38+ @patch('os.path.isfile')
39+ def test_filter_dir_no_opts(self, isfile, isdir):
40+ '''It filters out all non-py files by default'''
41+ result = self._test_filter_dir(opts=None, isfile=isfile, isdir=isdir)
42 ex = ['bad_file.bin', 'bad_file.img', 'some_dir']
43 self.assertEquals(ex, result)
44
45- @patch('tools.charm_helpers_sync.charm_helpers_sync._filter')
46+ @patch('os.path.isdir')
47+ @patch('os.path.isfile')
48+ def test_filter_dir_with_include(self, isfile, isdir):
49+ '''It includes non-py files if specified as an include opt'''
50+ result = self._test_filter_dir(opts=['inc=*.img'],
51+ isfile=isfile, isdir=isdir)
52+ ex = ['bad_file.bin', 'some_dir']
53+ self.assertEquals(ex, result)
54+
55+ @patch('os.path.isdir')
56+ @patch('os.path.isfile')
57+ def test_filter_dir_include_all(self, isfile, isdir):
58+ '''It does not filter anything if option specified to include all'''
59+ self.assertEquals(sync.get_filter(opts=['inc=*']), None)
60+
61+ @patch('tools.charm_helpers_sync.charm_helpers_sync.get_filter')
62 @patch('tools.charm_helpers_sync.charm_helpers_sync.ensure_init')
63 @patch('shutil.copytree')
64 @patch('shutil.rmtree')
65@@ -127,12 +147,13 @@
66 def test_sync_directory(self, exists, rmtree, copytree, ensure_init,
67 _filter):
68 '''It correctly syncs src directory to dest directory'''
69+ _filter.return_value = None
70 sync.sync_directory('/tmp/charm-helpers/charmhelpers/core',
71 'hooks/charmhelpers/core')
72 exists.return_value = True
73 rmtree.assert_called_with('hooks/charmhelpers/core')
74 copytree.assert_called_with('/tmp/charm-helpers/charmhelpers/core',
75- 'hooks/charmhelpers/core', ignore=_filter)
76+ 'hooks/charmhelpers/core', ignore=None)
77 ensure_init.assert_called_with('hooks/charmhelpers/core')
78
79 @patch('os.path.isfile')
80@@ -154,7 +175,7 @@
81
82 sync_dir.assert_called_with(
83 '/tmp/charm-helpers/charmhelpers/contrib/openstack',
84- 'hooks/charmhelpers/contrib/openstack')
85+ 'hooks/charmhelpers/contrib/openstack', None)
86
87 @patch('tools.charm_helpers_sync.charm_helpers_sync.sync_pyfile')
88 @patch('tools.charm_helpers_sync.charm_helpers_sync._is_pyfile')
89@@ -192,6 +213,37 @@
90 ]
91
92 ex_calls = []
93- [ex_calls.append(call('/tmp/charm-helpers', 'hooks/charmhelpers', c))
94- for c in mods]
95+ [ex_calls.append(
96+ call('/tmp/charm-helpers', 'hooks/charmhelpers', c, [])
97+ ) for c in mods]
98 self.assertEquals(ex_calls, _sync.call_args_list)
99+
100+ def test_extract_option_no_globals(self):
101+ '''It extracts option from an included item with no global options'''
102+ inc = 'contrib.openstack.templates|inc=*.template'
103+ result = sync.extract_options(inc)
104+ ex = ('contrib.openstack.templates', ['inc=*.template'])
105+ self.assertEquals(ex, result)
106+
107+ def test_extract_option_with_global_as_string(self):
108+ '''It extracts option for include with global options as str'''
109+ inc = 'contrib.openstack.templates|inc=*.template'
110+ result = sync.extract_options(inc, global_options='inc=foo.*')
111+ ex = ('contrib.openstack.templates',
112+ ['inc=*.template', 'inc=foo.*'])
113+ self.assertEquals(ex, result)
114+
115+ def test_extract_option_with_globals(self):
116+ '''It extracts option from an included item with global options'''
117+ inc = 'contrib.openstack.templates|inc=*.template'
118+ result = sync.extract_options(inc, global_options=['inc=*.cfg'])
119+ ex = ('contrib.openstack.templates', ['inc=*.template', 'inc=*.cfg'])
120+ self.assertEquals(ex, result)
121+
122+ def test_extract_multiple_options_with_globals(self):
123+ '''It extracts multiple options from an included item'''
124+ inc = 'contrib.openstack.templates|inc=*.template,inc=foo.*'
125+ result = sync.extract_options(inc, global_options=['inc=*.cfg'])
126+ ex = ('contrib.openstack.templates',
127+ ['inc=*.template', 'inc=foo.*', 'inc=*.cfg'])
128+ self.assertEquals(ex, result)
129
130=== modified file 'tools/charm_helpers_sync/README'
131--- tools/charm_helpers_sync/README 2013-06-27 22:21:29 +0000
132+++ tools/charm_helpers_sync/README 2013-07-16 21:31:23 +0000
133@@ -74,6 +74,31 @@
134 Script will create missing __init__.py's to ensure each subdirectory is
135 importable, assuming the script is run from the charm's top-level directory.
136
137+By default, only directories that look like python modules and associated
138+.py source files will be synced. If you need to include other files in
139+the sync (for example, template files), you can add include hints to
140+your config. This can be done either on a per-module basis using standard
141+UNIX filename patterns, eg:
142+
143+ destination: hooks/helpers
144+ branch: lp:charm-helpers
145+ include:
146+ - core|inc=* # include all extra files from this module.
147+ - contrib.openstack|inc=*.template # include only .template's
148+ - contrib.hahelpers:
149+ - ceph_utils|inc=*.cfg # include .cfg files
150+
151+Or globally for all included assets:
152+
153+ destination: hooks/helpers
154+ branch: lp:charm-helpers
155+ options: inc=*.template,inc=*.cfg # include .templates and .cfgs globally
156+ include:
157+ - core
158+ - contrib.openstack
159+ - contrib.hahelpers:
160+ - ceph_utils
161+
162 You may also override configured destination directory and source bzr
163 branch:
164
165
166=== modified file 'tools/charm_helpers_sync/charm_helpers_sync.py'
167--- tools/charm_helpers_sync/charm_helpers_sync.py 2013-06-27 22:02:19 +0000
168+++ tools/charm_helpers_sync/charm_helpers_sync.py 2013-07-16 21:31:23 +0000
169@@ -15,14 +15,18 @@
170 import tempfile
171 import yaml
172
173+from fnmatch import fnmatch
174+
175 CHARM_HELPERS_BRANCH = 'lp:charm-helpers'
176
177+
178 def parse_config(conf_file):
179 if not os.path.isfile(conf_file):
180 logging.error('Invalid config file: %s.' % conf_file)
181 return False
182 return yaml.load(open(conf_file).read())
183
184+
185 def clone_helpers(work_dir, branch):
186 dest = os.path.join(work_dir, 'charm-helpers')
187 logging.info('Checking out %s to %s.' % (branch, dest))
188@@ -30,31 +34,23 @@
189 subprocess.check_call(cmd)
190 return dest
191
192-def _filter(dir, ls):
193- _filter = []
194- for f in ls:
195- _f = os.path.join(dir, f)
196- if (os.path.isfile(_f) and not _f.endswith('.py')):
197- logging.debug('Not syncing files: %s' % f)
198- _filter.append(f)
199- elif (os.path.isdir(_f) and not
200- os.path.isfile(os.path.join(_f, '__init__.py'))):
201- logging.debug('Not syncing directory: %s' % f)
202- _filter.append(f)
203- return _filter
204
205 def _module_path(module):
206 return os.path.join(*module.split('.'))
207
208+
209 def _src_path(src, module):
210 return os.path.join(src, 'charmhelpers', _module_path(module))
211
212+
213 def _dest_path(dest, module):
214 return os.path.join(dest, _module_path(module))
215
216+
217 def _is_pyfile(path):
218 return os.path.isfile(path + '.py')
219
220+
221 def ensure_init(path):
222 '''
223 ensure directories leading up to path are importable, omitting
224@@ -69,6 +65,7 @@
225 logging.info('Adding missing __init__.py: %s' % _i)
226 open(_i, 'wb').close()
227
228+
229 def sync_pyfile(src, dest):
230 src = src + '.py'
231 src_dir = os.path.dirname(src)
232@@ -81,17 +78,51 @@
233 dest)
234 ensure_init(dest)
235
236-def sync_directory(src, dest):
237+
238+def get_filter(opts=None):
239+ opts = opts or []
240+ if 'inc=*' in opts:
241+ # do not filter any files, include everything
242+ return None
243+
244+ def _filter(dir, ls):
245+ incs = [opt.split('=').pop() for opt in opts if 'inc=' in opt]
246+ _filter = []
247+ for f in ls:
248+ _f = os.path.join(dir, f)
249+
250+ if not os.path.isdir(_f) and not _f.endswith('.py') and incs:
251+ if True not in [fnmatch(_f, inc) for inc in incs]:
252+ logging.debug('Not syncing %s, does not match include '
253+ 'filters (%s)' % (_f, incs))
254+ _filter.append(f)
255+ else:
256+ logging.debug('Including file, which matches include '
257+ 'filters (%s): %s' % (incs, _f))
258+ elif (os.path.isfile(_f) and not _f.endswith('.py')):
259+ logging.debug('Not syncing file: %s' % f)
260+ _filter.append(f)
261+ elif (os.path.isdir(_f) and not
262+ os.path.isfile(os.path.join(_f, '__init__.py'))):
263+ logging.debug('Not syncing directory: %s' % f)
264+ _filter.append(f)
265+ return _filter
266+ return _filter
267+
268+
269+def sync_directory(src, dest, opts=None):
270 if os.path.exists(dest):
271 logging.debug('Removing existing directory: %s' % dest)
272 shutil.rmtree(dest)
273 logging.info('Syncing directory: %s -> %s.' % (src, dest))
274- shutil.copytree(src, dest, ignore=_filter)
275+
276+ shutil.copytree(src, dest, ignore=get_filter(opts))
277 ensure_init(dest)
278
279-def sync(src, dest, module):
280+
281+def sync(src, dest, module, opts=None):
282 if os.path.isdir(_src_path(src, module)):
283- sync_directory(_src_path(src, module), _dest_path(dest, module))
284+ sync_directory(_src_path(src, module), _dest_path(dest, module), opts)
285 elif _is_pyfile(_src_path(src, module)):
286 sync_pyfile(_src_path(src, module),
287 os.path.dirname(_dest_path(dest, module)))
288@@ -99,18 +130,40 @@
289 logging.warn('Could not sync: %s. Neither a pyfile or directory, '
290 'does it even exist?' % module)
291
292-def sync_helpers(include, src, dest):
293+
294+def parse_sync_options(options):
295+ if not options:
296+ return []
297+ return options.split(',')
298+
299+
300+def extract_options(inc, global_options=None):
301+ global_options = global_options or []
302+ if global_options and isinstance(global_options, basestring):
303+ global_options = [global_options]
304+ if '|' not in inc:
305+ return (inc, global_options)
306+ inc, opts = inc.split('|')
307+ return (inc, parse_sync_options(opts) + global_options)
308+
309+
310+def sync_helpers(include, src, dest, options=None):
311 if not os.path.isdir(dest):
312 os.mkdir(dest)
313
314+ global_options = parse_sync_options(options)
315+
316 for inc in include:
317 if isinstance(inc, str):
318- sync(src, dest, inc)
319+ inc, opts = extract_options(inc, global_options)
320+ sync(src, dest, inc, opts)
321 elif isinstance(inc, dict):
322 # could also do nested dicts here.
323 for k, v in inc.iteritems():
324 if isinstance(v, list):
325- [sync(src, dest, '%s.%s' % (k, m)) for m in v]
326+ for m in v:
327+ inc, opts = extract_options(m, global_options)
328+ sync(src, dest, '%s.%s' % (k, inc), opts)
329
330 if __name__ == '__main__':
331 parser = optparse.OptionParser()
332@@ -156,10 +209,14 @@
333 config['include'] = []
334 [config['include'].append(a) for a in args]
335
336+ sync_options = None
337+ if 'options' in config:
338+ sync_options = config['options']
339 tmpd = tempfile.mkdtemp()
340 try:
341 checkout = clone_helpers(tmpd, config['branch'])
342- sync_helpers(config['include'], checkout, config['destination'])
343+ sync_helpers(config['include'], checkout, config['destination'],
344+ options=sync_options)
345 except Exception, e:
346 logging.error("Could not sync: %s" % e)
347 raise e

Subscribers

People subscribed via source and target branches