Merge lp:~allenap/bzr-update-copyright/force-ranges into lp:bzr-update-copyright

Proposed by Gavin Panella
Status: Merged
Approved by: Richard Wilbur
Approved revision: 19
Merged at revision: 14
Proposed branch: lp:~allenap/bzr-update-copyright/force-ranges
Merge into: lp:bzr-update-copyright
Diff against target: 427 lines (+145/-61)
3 files modified
__init__.py (+14/-12)
test_update_copyright.py (+94/-30)
update_copyright.py (+37/-19)
To merge this branch: bzr merge lp:~allenap/bzr-update-copyright/force-ranges
Reviewer Review Type Date Requested Status
Richard Wilbur Approve
Review via email: mp+257236@code.launchpad.net

Description of the change

This branch adds a --force-ranges option (which I'm happy to rename) which always formats years as $min-$max or $year; it ignores gaps. This is how we do it on MAAS because it means we don't have to re-wrap copyright stanzas to appease the linter. It may not be "proper" but it's good enough.

I've also cleaned up all PEP8 warnings. I can revert these changes if they're unwanted.

To post a comment you must log in.
Revision history for this message
Richard Wilbur (richard-wilbur) wrote :

Since I don't see a bug number that this is solving, a little background would be useful to help me understand. You mention "MAAS" as formatting years in the copyright statement either as a range or a single year. What is "MAAS"?

My second question regarding copyright dates is, "What is 'proper' and how hard would that be to implement?"

Revision history for this message
Gavin Panella (allenap) wrote :

> Since I don't see a bug number that this is solving, a little
> background would be useful to help me understand. You mention "MAAS"
> as formatting years in the copyright statement either as a range or a
> single year. What is "MAAS"?

MAAS is https://launchpad.net/maas. We have a standard copyright header
at the top of each file. Naturally it begins life with a single year,
but that can later be substituted with a date range, "2012-2015" or
"2012, 2013" for example, without causing the line to extend to more
than 80 characters.

When there are three years and they're not all contiguous with one
another, bzr-update-copyright will render it longer, as "2012, 2013,
2015" for example. For us it means that the line grows long enough that
our linter complains, meaning we would have to reflow the header.

It sounds really lazy to have an issue with that, and I kind of agree,
but then I also don't want to replace one manual job (changing the
header dates) with another (reflowing the paragraph), especially when
forgetting to do the latter would cause our landing bot to reject
submissions.

In any case, we've always used ${year-created}-${year-last-modified} in
MAAS, albeit manually. We would be happy to condense the non-contiguous
example from earlier into "2012-2015", even though no changes were made
in 2014.

So, this isn't in response to a bug, it's an itch that I scratched.

> My second question regarding copyright dates is, "What is 'proper' and
> how hard would that be to implement?"

"Proper" may have been an overstatement. I think of the current
behaviour of this plugin as "proper", and --force-range as somewhat
"improper", but I don't actually know which is more proper than the
other. I was motivated only to automate our current practice without
disrupting existing users of bzr-update-copyright.

Revision history for this message
Richard Wilbur (richard-wilbur) wrote :

Thanks for the background.

I have a question regarding line 32 of __init__.py: Why is this the only line of option description text delimited by double quotes(")? All the others seems to use single quotes(').

Other than that, it looks good to me.

19. By Gavin Panella

Make quoting consistent.

Revision history for this message
Gavin Panella (allenap) wrote :

Thanks Richard. I've fixed the quotes to be consistent.

Revision history for this message
Richard Wilbur (richard-wilbur) wrote :

Thank you for the PEP8 clean up and the new option and documentation.
+1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '__init__.py'
2--- __init__.py 2014-04-10 08:16:12 +0000
3+++ __init__.py 2015-05-07 09:22:34 +0000
4@@ -28,12 +28,10 @@
5 want it active everywhere.
6 """
7
8-
9 from bzrlib import (
10 commands,
11- errors,
12 _format_version_tuple,
13- mutabletree, # I wish we could make the hook registration fully lazy
14+ mutabletree, # I wish we could make the hook registration fully lazy
15 option,
16 trace,
17 )
18@@ -56,12 +54,16 @@
19 """
20
21 takes_args = ['path*']
22- takes_options = [option.Option('only-changed',
23- help='Only update the copyright for files which are'
24- ' marked as modified.'),
25- ]
26+ takes_options = [
27+ option.Option(
28+ 'only-changed', help='Only update the copyright for files which '
29+ 'are marked as modified.'),
30+ option.Option(
31+ 'force-range', help='Only update the copyright with a range of '
32+ 'years, i.e. ignore years in which no change was made.'),
33+ ]
34
35- def run(self, path_list=None, only_changed=False):
36+ def run(self, path_list=None, only_changed=False, force_range=False):
37 from bzrlib import osutils, workingtree
38 import update_copyright
39
40@@ -72,8 +74,8 @@
41 relpaths = None
42 if relpaths is not None:
43 relpaths = osutils.minimum_path_selection(relpaths)
44- result = update_copyright.update_tree_copyright(tree, relpaths,
45- only_changed=only_changed)
46+ result = update_copyright.update_tree_copyright(
47+ tree, relpaths, only_changed=only_changed, force_range=force_range)
48 self.outf.write('Checked %(count)d files\n'
49 'Updated %(updated)d\n'
50 'Already correct %(copyright-correct)d\n'
51@@ -105,8 +107,8 @@
52 % res)
53
54
55-mutabletree.MutableTree.hooks.install_named_hook('start_commit',
56- _update_copyright_start_commit_hook, 'update-copyright')
57+mutabletree.MutableTree.hooks.install_named_hook(
58+ 'start_commit', _update_copyright_start_commit_hook, 'update-copyright')
59
60
61 def load_tests(standard_tests, module, loader):
62
63=== modified file 'test_update_copyright.py'
64--- test_update_copyright.py 2014-04-10 08:10:39 +0000
65+++ test_update_copyright.py 2015-05-07 09:22:34 +0000
66@@ -13,12 +13,11 @@
67 # You should have received a copy of the GNU General Public License
68 # along with this program; if not, write to the Free Software
69 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
70-#
71+#
72
73 import datetime
74
75 from bzrlib import (
76- errors,
77 mutabletree,
78 tests,
79 )
80@@ -80,6 +79,18 @@
81 self.assertFormatYears('2005, 2007-2010',
82 [2005, 2007, 2008, 2009, 2010])
83
84+ def assertFormatYearsAsRange(self, expected, years):
85+ self.assertEqual(
86+ expected, update_copyright.format_years_as_range(set(years)))
87+
88+ def test_format_years_as_range(self):
89+ self.assertFormatYearsAsRange('2008-2009', [2008, 2009])
90+ self.assertFormatYearsAsRange('2007-2009', [2007, 2009])
91+ self.assertFormatYearsAsRange('2007-2009', [2007, 2008, 2009])
92+ self.assertFormatYearsAsRange('2007-2010', [2007, 2008, 2009, 2010])
93+ self.assertFormatYearsAsRange(
94+ '2005-2010', [2005, 2007, 2008, 2009, 2010])
95+
96
97 class TestUpdateCopyrightDisk(tests.TestCaseWithTransport):
98
99@@ -87,24 +98,26 @@
100 t = self.make_branch_and_tree('.')
101 t.lock_write()
102 self.addCleanup(t.unlock)
103- self.build_tree_contents([('file',
104- 'Copyright (c) 2008 Foo Bar\nsome content\nmore content\n')])
105+ self.build_tree_contents([
106+ ('file',
107+ 'Copyright (c) 2008 Foo Bar\nsome content\nmore content\n')])
108 t.add(['file'], ['file-id'])
109 rev_ids = []
110 rev_ids.append(
111- t.commit('new file', timestamp=1199817864, timezone=0)) # 2008
112- self.build_tree_contents([('file',
113- 'Copyright (c) 2008 Foo Bar\ndifferent content\n')])
114+ t.commit('new file', timestamp=1199817864, timezone=0)) # 2008
115+ self.build_tree_contents([
116+ ('file', 'Copyright (c) 2008 Foo Bar\ndifferent content\n')])
117 rev_ids.append(
118- t.commit('mod file', timestamp=1231353864, timezone=0)) # 2009
119+ t.commit('mod file', timestamp=1231353864, timezone=0)) # 2009
120 return t, rev_ids
121
122 def make_old_modified_but_correct_tree(self):
123 t, rev_ids = self.make_old_modified_tree()
124- self.build_tree_contents([('file',
125- 'Copyright (c) 2008, 2009 Foo Bar\ndifferent content\n')])
126- rev_ids.append(
127- t.commit('fix copyright', timestamp=1231353865, timezone=0)) # 2009
128+ self.build_tree_contents([
129+ ('file', 'Copyright (c) 2008, 2009 Foo Bar\ndifferent content\n'),
130+ ])
131+ rev_ids.append(t.commit(
132+ 'fix copyright', timestamp=1231353865, timezone=0)) # 2009
133 return t, rev_ids
134
135 def make_tree_with_dirs(self):
136@@ -112,15 +125,15 @@
137 self.build_tree(['dir/'])
138 self.build_tree_contents([('dir/file', 'Copyright 2008 Foo\r\n')])
139 t.add(['dir', 'dir/file'], ['dir-id', 'dfile-id'])
140- rev_ids.append(
141- t.commit('dir and file', timestamp=1199817865, timezone=0)) # 2008
142+ rev_ids.append(t.commit(
143+ 'dir and file', timestamp=1199817865, timezone=0)) # 2008
144 self.build_tree(['dir/subdir/'])
145 self.build_tree_contents([
146 ('dir/file', 'Copyright 2008 Foo\r\nnew content\r\n'),
147 ('dir/subdir/foo', 'Copyright 2008 Bar\ncontent\n')])
148 t.add(['dir/subdir', 'dir/subdir/foo'], ['subdir-id', 'foo-id'])
149 rev_ids.append(
150- t.commit('mods', timestamp=1231353866, timezone=0)) # 2009
151+ t.commit('mods', timestamp=1231353866, timezone=0)) # 2009
152 return t, rev_ids
153
154 def make_tree_with_dirs_modified(self):
155@@ -160,12 +173,24 @@
156 self.assertFileEqual('Copyright (c) 2008, 2009, %d Foo Bar\n'
157 'different content\n' % (year,), 'file')
158
159+ def test_update_copyright_with_ranges(self):
160+ t, rev_ids = self.make_old_modified_tree()
161+ bt = t.basis_tree()
162+ bt.lock_read()
163+ self.addCleanup(bt.unlock)
164+ self.assertEqual(
165+ 'updated', update_copyright.update_copyright(
166+ t, bt, 'file', False, update_copyright.format_years_as_range))
167+ year = datetime.datetime.now().year
168+ self.assertFileEqual('Copyright (c) 2008-%d Foo Bar\n'
169+ 'different content\n' % (year,), 'file')
170+
171 def test_update_copyright_not_first_line(self):
172 t, rev_ids = self.make_old_modified_tree()
173- self.build_tree_contents([('file',
174- '\n\n\nCopyright (c) 2008 Foo Bar\nultimate content\n')])
175+ self.build_tree_contents([
176+ ('file', '\n\n\nCopyright (c) 2008 Foo Bar\nultimate content\n')])
177 rev_ids.append(
178- t.commit('remod file', timestamp=1231353864, timezone=0)) # 2010
179+ t.commit('remod file', timestamp=1231353864, timezone=0)) # 2010
180 bt = t.basis_tree()
181 bt.lock_read()
182 self.addCleanup(bt.unlock)
183@@ -178,10 +203,11 @@
184
185 def test_update_copyright_after_max_line(self):
186 t, rev_ids = self.make_old_modified_tree()
187- self.build_tree_contents([('file',
188- '\n\n\n\n\nCopyright (c) 2008 Foo Bar\nultimate content\n')])
189+ self.build_tree_contents([
190+ ('file',
191+ '\n\n\n\n\nCopyright (c) 2008 Foo Bar\nultimate content\n')])
192 rev_ids.append(
193- t.commit('remod file', timestamp=1231353864, timezone=0)) # 2010
194+ t.commit('remod file', timestamp=1231353864, timezone=0)) # 2010
195 bt = t.basis_tree()
196 bt.lock_read()
197 self.addCleanup(bt.unlock)
198@@ -280,9 +306,19 @@
199 self.assertFileEqual('Copyright 2008 Bar\n'
200 'content\n', 'dir/subdir/foo')
201
202+ def check_tree_with_dirs_modified_using_ranges(self):
203+ year = datetime.datetime.now().year
204+ self.assertFileEqual('Copyright (c) 2008 Foo Bar\n'
205+ 'different content\n', 'file')
206+ self.assertFileEqual('Copyright 2008-%d Foo\r\n'
207+ 'modified content\r\n' % (year,), 'dir/file')
208+ self.assertFileEqual('Copyright 2008 Bar\n'
209+ 'content\n', 'dir/subdir/foo')
210+
211 def test_update_tree_only_changed(self):
212 t, rev_ids = self.make_tree_with_dirs_modified()
213- res = update_copyright.update_tree_copyright(t, None, only_changed=True)
214+ res = update_copyright.update_tree_copyright(
215+ t, None, only_changed=True)
216 self.check_tree_with_dirs_modified()
217 self.assertEqual({'count': 1, 'updated': 1, 'no-copyright': 0,
218 'copyright-correct': 0, 'not-versioned': 0},
219@@ -292,7 +328,8 @@
220 t, rev_ids = self.make_tree_with_dirs_modified()
221 self.build_tree_contents([('file2', 'Copyright 1700 Fooz\ncontent\n')])
222 t.add(['file2'])
223- res = update_copyright.update_tree_copyright(t, None, only_changed=True)
224+ res = update_copyright.update_tree_copyright(
225+ t, None, only_changed=True)
226 year = datetime.datetime.now().year
227 self.check_tree_with_dirs_modified()
228 self.assertEqual({'count': 2, 'updated': 2, 'no-copyright': 0,
229@@ -307,7 +344,8 @@
230 # modified versus the committed content
231 self.build_tree_contents([
232 ('dir/file', 'Copyright 2008, 2009 Foo\r\nmodified content\r\n')])
233- res = update_copyright.update_tree_copyright(t, None, only_changed=True)
234+ res = update_copyright.update_tree_copyright(
235+ t, None, only_changed=True)
236 self.check_tree_with_dirs_modified()
237 self.assertEqual({'count': 1, 'updated': 1, 'no-copyright': 0,
238 'copyright-correct': 0, 'not-versioned': 0},
239@@ -317,8 +355,9 @@
240 t, rev_ids = self.make_tree_with_dirs_modified()
241 # We have to install it here, because the test suite clears out hooks
242 # as part of its isolation mechanism.
243- mutabletree.MutableTree.hooks.install_named_hook('start_commit',
244- _update_copyright_start_commit_hook, 'update-copyright')
245+ mutabletree.MutableTree.hooks.install_named_hook(
246+ 'start_commit', _update_copyright_start_commit_hook,
247+ 'update-copyright')
248 # Without any config, nothing should be modified.
249 t.commit('Modified content')
250 self.assertFileEqual('Copyright (c) 2008 Foo Bar\n'
251@@ -332,8 +371,9 @@
252 t, rev_ids = self.make_tree_with_dirs_modified()
253 # We have to install it here, because the test suite clears out hooks
254 # as part of its isolation mechanism.
255- mutabletree.MutableTree.hooks.install_named_hook('start_commit',
256- _update_copyright_start_commit_hook, 'update-copyright')
257+ mutabletree.MutableTree.hooks.install_named_hook(
258+ 'start_commit', _update_copyright_start_commit_hook,
259+ 'update-copyright')
260 t.branch.get_config().set_user_option('auto_update_copyright', 'True')
261 t.commit('Modified content')
262 # With the hook enabled, commit should update the copyright in the
263@@ -353,6 +393,19 @@
264 self.assertFileEqual('Copyright (c) 2008, 2009, %d Foo Bar\n'
265 'different content\n' % (year,), 'file')
266
267+ def test_blackbox_force_range(self):
268+ t, rev_ids = self.make_old_modified_tree()
269+ # We need to unlock so that run_bzr can grab a lock
270+ t.unlock()
271+ try:
272+ self.run_bzr('update-copyright --force-range file')
273+ finally:
274+ # leave the tree locked, because we have pending cleanup
275+ t.lock_read()
276+ year = datetime.datetime.now().year
277+ self.assertFileEqual('Copyright (c) 2008-%d Foo Bar\n'
278+ 'different content\n' % (year,), 'file')
279+
280 def test_blackbox_only_changed(self):
281 t, rev_ids = self.make_tree_with_dirs_modified()
282 t.unlock()
283@@ -363,10 +416,21 @@
284 t.lock_read()
285 self.check_tree_with_dirs_modified()
286
287+ def test_blackbox_only_changed_force_range(self):
288+ t, rev_ids = self.make_tree_with_dirs_modified()
289+ t.unlock()
290+ try:
291+ self.run_bzr('update-copyright --only-changed --force-range')
292+ finally:
293+ # leave the tree locked, because we have pending cleanup
294+ t.lock_read()
295+ self.check_tree_with_dirs_modified_using_ranges()
296+
297 def test_blackbox_with_commit_hook(self):
298 t, rev_ids = self.make_tree_with_dirs_modified()
299- mutabletree.MutableTree.hooks.install_named_hook('start_commit',
300- _update_copyright_start_commit_hook, 'update-copyright')
301+ mutabletree.MutableTree.hooks.install_named_hook(
302+ 'start_commit', _update_copyright_start_commit_hook,
303+ 'update-copyright')
304 t.branch.get_config().set_user_option('auto_update_copyright', 'True')
305 t.unlock()
306 try:
307
308=== modified file 'update_copyright.py'
309--- update_copyright.py 2014-04-10 08:10:39 +0000
310+++ update_copyright.py 2015-05-07 09:22:34 +0000
311@@ -13,12 +13,11 @@
312 # You should have received a copy of the GNU General Public License
313 # along with this program; if not, write to the Free Software
314 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
315-#
316+#
317
318 """Tools for updating the copyright of given files."""
319
320-import cStringIO
321-import datetime
322+from datetime import datetime
323 import re
324
325 from bzrlib import (
326@@ -39,13 +38,12 @@
327 """Determine the dates a given file was modified."""
328 tip = (file_id, last_rev_id)
329 ancestry = graph.Graph(repository.texts).iter_ancestry([tip])
330- revision_ids = set([k[1] for k,p in ancestry if p is not None])
331+ revision_ids = set(k[1] for k, p in ancestry if p is not None)
332 # Oddly enough, caching the Revision object *slows* it down. My guess is
333 # that it interacts poorly with gc
334 revisions = repository.get_revisions(revision_ids)
335 # TODO: Should we use utcfromtimestamp?
336- years = set([datetime.datetime.fromtimestamp(r.timestamp).year
337- for r in revisions])
338+ years = set(datetime.fromtimestamp(r.timestamp).year for r in revisions)
339 return years
340
341
342@@ -76,6 +74,14 @@
343 return ', '.join(out_years)
344
345
346+def format_years_as_range(years):
347+ """Format a bunch of years back into a date range string."""
348+ if len(years) == 1:
349+ return '%d' % years.pop()
350+ else:
351+ return '%d-%d' % (min(years), max(years))
352+
353+
354 def _iter_all_files(tree, relpaths, basis_tree):
355 file_ids = tree.paths2ids(relpaths, trees=[basis_tree])
356 for path, ie in tree.iter_entries_by_dir(file_ids):
357@@ -90,18 +96,25 @@
358 yield path[1]
359
360
361-def update_tree_copyright(tree, relpaths, only_changed=False):
362+def update_tree_copyright(
363+ tree, relpaths, only_changed=False, force_range=False):
364 """Update all of the paths specified in the tree."""
365- result = {'not-versioned': 0,
366- 'no-copyright': 0,
367- 'copyright-correct': 0,
368- 'updated': 0,
369- 'count': 0,
370- }
371+ if force_range:
372+ fmt = format_years_as_range
373+ else:
374+ fmt = format_years
375+
376+ result = {
377+ 'not-versioned': 0,
378+ 'no-copyright': 0,
379+ 'copyright-correct': 0,
380+ 'updated': 0,
381+ 'count': 0,
382+ }
383 # We only *need* lock_tree_write, however the
384 # WorkingTree.put_file_bytes_non_atomic api uses a @needs_lock_write
385 # decorator, which means it tries to write lock the branch...
386- #tree.lock_tree_write()
387+ # tree.lock_tree_write()
388 tree.lock_write()
389 try:
390 basis_tree = tree.basis_tree()
391@@ -117,8 +130,9 @@
392 result['count'] += 1
393 # If we are iterating changed files only, then we know this
394 # file is changed
395- action = update_copyright(tree, basis_tree, path,
396- is_changed=only_changed)
397+ action = update_copyright(
398+ tree, basis_tree, path, is_changed=only_changed,
399+ format_years=fmt)
400 result[action] += 1
401 pb.update('%s %s' % (action, path), result[action],
402 result['count'])
403@@ -131,17 +145,21 @@
404 return result
405
406
407-def update_copyright(tree, basis_tree, filename, is_changed=False):
408+def update_copyright(
409+ tree, basis_tree, filename, is_changed=False,
410+ format_years=format_years):
411 """Update the copyright line for a given file.
412-
413+
414 :param is_changed: If we know the file is changed, then we know it must
415 have this years date in the Copyright header. If it is unknown if the
416 file has changed, just pass False.
417+ :param format_years: The function to use to format a set of years into a
418+ string for inclusion in the copyright header.
419 """
420 file_id = tree.path2id(filename)
421 if file_id is None:
422 return 'not-versioned'
423- this_year = datetime.datetime.now().year
424+ this_year = datetime.now().year
425 f = tree.get_file(file_id, path=filename)
426 max_lines = 5
427 cur_line = 0

Subscribers

People subscribed via source and target branches