Skip to content

Commit 2c6a76e

Browse files
authored
Fix few --use-current-year bugs (#63)
1 parent 705ebee commit 2c6a76e

4 files changed

Lines changed: 145 additions & 64 deletions

File tree

pre_commit_hooks/insert_license.py

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@
3232
])
3333

3434

35+
class LicenseUpdateError(Exception):
36+
def __init__(self, message):
37+
super().__init__(message)
38+
self.message = message
39+
40+
3541
def main(argv=None):
3642
parser = argparse.ArgumentParser()
3743
parser.add_argument('filenames', nargs='*', help='filenames to check')
@@ -137,8 +143,9 @@ def process_files(args, changed_files, todo_files, license_info: LicenseInfo):
137143
:param changed_files: list of changed files
138144
:param todo_files: list of files where t.o.d.o. is detected
139145
:param license_info: license info named tuple
140-
:return: True if some files were changed or t.o.d.o is detected
146+
:return: True if some files were changed, t.o.d.o is detected or an error occurred while updating the year
141147
"""
148+
license_update_failed = False
142149
after_regex = args.insert_license_after_regex
143150
for src_filepath in args.filenames:
144151
src_file_content, encoding = _read_file_content(src_filepath)
@@ -168,14 +175,18 @@ def process_files(args, changed_files, todo_files, license_info: LicenseInfo):
168175
fuzzy_ratio_cut_off=args.fuzzy_ratio_cut_off,
169176
)
170177
if license_header_index is not None:
171-
if license_found(remove_header=args.remove_header,
172-
update_year_range=args.use_current_year,
173-
license_header_index=license_header_index,
174-
license_info=license_info,
175-
src_file_content=src_file_content,
176-
src_filepath=src_filepath,
177-
encoding=encoding):
178-
changed_files.append(src_filepath)
178+
try:
179+
if license_found(remove_header=args.remove_header,
180+
update_year_range=args.use_current_year,
181+
license_header_index=license_header_index,
182+
license_info=license_info,
183+
src_file_content=src_file_content,
184+
src_filepath=src_filepath,
185+
encoding=encoding):
186+
changed_files.append(src_filepath)
187+
except LicenseUpdateError as error:
188+
print(error)
189+
license_update_failed = True
179190
else:
180191
if fuzzy_match_header_index is not None:
181192
if fuzzy_license_found(license_info=license_info,
@@ -194,7 +205,7 @@ def process_files(args, changed_files, todo_files, license_info: LicenseInfo):
194205
encoding=encoding,
195206
after_regex=after_regex):
196207
changed_files.append(src_filepath)
197-
return changed_files or todo_files
208+
return changed_files or todo_files or license_update_failed
198209

199210

200211
def _read_file_content(src_filepath):
@@ -261,7 +272,9 @@ def license_not_found( # pylint: disable=too-many-arguments
261272

262273
def try_update_year_range(
263274
src_file_content: list[str],
275+
src_filepath: str,
264276
license_header_index: int,
277+
license_length: int
265278
) -> tuple[Sequence[str], bool]:
266279
"""
267280
Updates the years in a copyright header in src_file_content by
@@ -274,22 +287,25 @@ def try_update_year_range(
274287
:return: source file contents and a flag indicating update
275288
"""
276289
current_year = datetime.now().year
277-
for i in range(license_header_index, len(src_file_content)):
290+
for i in range(license_header_index, license_header_index + license_length):
278291
line = src_file_content[i]
279292
matches = _YEAR_RANGE_PATTERN.findall(line)
280293
if matches:
281294
match = matches[-1]
282295
start_year = int(match[:4])
283-
end_year = match[5:]
284-
if not end_year or int(end_year) < current_year:
296+
end_year = match[5:].lstrip(" -,")
297+
if not end_year and start_year < current_year or end_year and int(end_year) < current_year:
285298
updated = line.replace(match,
286-
str(start_year) + '-' + str(current_year))
299+
str(start_year) + '-' + str(current_year))
287300
# verify the current list of years ends in the current one
288301
if _YEARS_PATTERN.findall(updated)[-1][-4:] != str(current_year):
289-
print(f"Unable to update year range in line: {line.rstrip()}. Got: {updated.rstrip()}")
290-
break
302+
raise LicenseUpdateError(
303+
f"Year range detected in license header, but we were unable to update it.\n"
304+
f"File: {src_filepath}\nInput line: {line.rstrip()}\nDiscarded result: {updated.rstrip()}"
305+
)
291306
src_file_content[i] = updated
292307
return src_file_content, True
308+
break
293309
return src_file_content, False
294310

295311

@@ -324,7 +340,7 @@ def license_found(
324340
len(license_info.prefixed_license) + 1:]
325341
updated = True
326342
elif update_year_range:
327-
src_file_content, updated = try_update_year_range(src_file_content, license_header_index)
343+
src_file_content, updated = try_update_year_range(src_file_content, src_filepath, license_header_index, len(license_info.prefixed_license))
328344

329345
if updated:
330346
with open(src_filepath, 'w', encoding=encoding) as src_file:

tests/insert_license_test.py

Lines changed: 100 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from contextlib import contextmanager
22
from datetime import datetime
33
from itertools import product
4+
import io
45
import os
56
import shutil
7+
import sys
68
import pytest
79

810
from pre_commit_hooks.insert_license import main as insert_license, LicenseInfo
@@ -12,67 +14,81 @@
1214

1315

1416
@pytest.mark.parametrize(
15-
('license_file_path', 'src_file_path', 'comment_prefix', 'new_src_file_expected', 'fail_check', 'extra_args'),
16-
map(lambda a: a[:1] + a[1], product( # combine license files with other args
17-
('LICENSE_with_trailing_newline.txt', 'LICENSE_without_trailing_newline.txt'),
18-
(
19-
('module_without_license.py', '#', 'module_with_license.py', True, None),
20-
('module_without_license_skip.py', '#', None, False, None),
21-
('module_with_license.py', '#', None, False, None),
22-
('module_with_license_todo.py', '#', None, True, None),
23-
24-
('module_without_license.jinja', '{#||#}', 'module_with_license.jinja', True, None),
25-
('module_without_license_skip.jinja', '{#||#}', None, False, None),
26-
('module_with_license.jinja', '{#||#}', None, False, None),
27-
('module_with_license_todo.jinja', '{#||#}', None, True, None),
28-
29-
('module_without_license_and_shebang.py', '#', 'module_with_license_and_shebang.py', True, None),
30-
('module_without_license_and_shebang_skip.py', '#', None, False, None),
31-
('module_with_license_and_shebang.py', '#', None, False, None),
32-
('module_with_license_and_shebang_todo.py', '#', None, True, None),
33-
34-
('module_without_license.groovy', '//', 'module_with_license.groovy', True, None),
35-
('module_without_license_skip.groovy', '//', None, False, None),
36-
('module_with_license.groovy', '//', None, False, None),
37-
('module_with_license_todo.groovy', '//', None, True, None),
38-
39-
('module_without_license.css', '/*| *| */', 'module_with_license.css', True, None),
40-
('module_without_license_and_few_words.css', '/*| *| */',
41-
'module_with_license_and_few_words.css', True, None), # Test fuzzy match does not match greedily
42-
('module_without_license_skip.css', '/*| *| */', None, False, None),
43-
('module_with_license.css', '/*| *| */', None, False, None),
44-
('module_with_license_todo.css', '/*| *| */', None, True, None),
45-
46-
('main_without_license.cpp', '/*|\t| */', 'main_with_license.cpp', True, None),
47-
('main_iso8859_without_license.cpp', '/*|\t| */', 'main_iso8859_with_license.cpp', True, None),
48-
('module_without_license.txt', '', 'module_with_license_noprefix.txt', True, None),
49-
('module_without_license.py', '#', 'module_with_license_nospace.py', True, ['--no-space-in-comment-prefix']),
50-
('module_without_license.php', '/*| *| */', 'module_with_license.php', True, ['--insert-license-after-regex', '^<\\?php$']),
51-
('module_without_license.py', '#', 'module_with_license_noeol.py', True, ['--no-extra-eol']),
52-
53-
('module_without_license.groovy', '//', 'module_with_license.groovy', True, ['--use-current-year']),
54-
('module_with_stale_year_in_license.py', '#', 'module_with_year_range_in_license.py', True, ['--use-current-year']),
55-
('module_with_stale_year_range_in_license.py', '#', 'module_with_year_range_in_license.py', True, ['--use-current-year']),
56-
('module_with_badly_formatted_stale_year_range_in_license.py', '#', 'module_with_badly_formatted_stale_year_range_in_license.py', False,
57-
['--use-current-year']),
17+
("license_file_path", "src_file_path", "comment_prefix", "new_src_file_expected", "message_expected", "fail_check", "extra_args"),
18+
map(
19+
lambda a: a[:1] + a[1],
20+
product( # combine license files with other args
21+
("LICENSE_with_trailing_newline.txt", "LICENSE_without_trailing_newline.txt"),
22+
(
23+
("module_without_license.py", "#", "module_with_license.py", "", True, None),
24+
("module_without_license_skip.py", "#", None, "", False, None),
25+
("module_with_license.py", "#", None, "", False, None),
26+
("module_with_license_todo.py", "#", None, "", True, None),
27+
("module_without_license.jinja", "{#||#}", "module_with_license.jinja", "", True, None),
28+
("module_without_license_skip.jinja", "{#||#}", None, "", False, None),
29+
("module_with_license.jinja", "{#||#}", None, "", False, None),
30+
("module_with_license_todo.jinja", "{#||#}", None, "", True, None),
31+
("module_without_license_and_shebang.py", "#", "module_with_license_and_shebang.py", "", True, None),
32+
("module_without_license_and_shebang_skip.py", "#", None, "", False, None),
33+
("module_with_license_and_shebang.py", "#", None, "", False, None),
34+
("module_with_license_and_shebang_todo.py", "#", None, "", True, None),
35+
("module_without_license.groovy", "//", "module_with_license.groovy", "", True, None),
36+
("module_without_license_skip.groovy", "//", None, "", False, None),
37+
("module_with_license.groovy", "//", None, "", False, None),
38+
("module_with_license_todo.groovy", "//", None, "", True, None),
39+
("module_without_license.css", "/*| *| */", "module_with_license.css", "", True, None),
40+
(
41+
"module_without_license_and_few_words.css",
42+
"/*| *| */",
43+
"module_with_license_and_few_words.css",
44+
"",
45+
True,
46+
None,
47+
), # Test fuzzy match does not match greedily
48+
("module_without_license_skip.css", "/*| *| */", None, "", False, None),
49+
("module_with_license.css", "/*| *| */", None, "", False, None),
50+
("module_with_license_todo.css", "/*| *| */", None, "", True, None),
51+
("main_without_license.cpp", "/*|\t| */", "main_with_license.cpp", "", True, None),
52+
("main_iso8859_without_license.cpp", "/*|\t| */", "main_iso8859_with_license.cpp", "", True, None),
53+
("module_without_license.txt", "", "module_with_license_noprefix.txt", "", True, None),
54+
("module_without_license.py", "#", "module_with_license_nospace.py", "", True, ["--no-space-in-comment-prefix"]),
55+
("module_without_license.php", "/*| *| */", "module_with_license.php", "", True, ["--insert-license-after-regex", "^<\\?php$"]),
56+
("module_without_license.py", "#", "module_with_license_noeol.py", "", True, ["--no-extra-eol"]),
57+
("module_without_license.groovy", "//", "module_with_license.groovy", "", True, ["--use-current-year"]),
58+
("module_with_stale_year_in_license.py", "#", "module_with_year_range_in_license.py", "", True, ["--use-current-year"]),
59+
("module_with_stale_year_range_in_license.py", "#", "module_with_year_range_in_license.py", "", True, ["--use-current-year"]),
60+
(
61+
"module_with_badly_formatted_stale_year_range_in_license.py",
62+
"#",
63+
"module_with_badly_formatted_stale_year_range_in_license.py",
64+
"module_with_badly_formatted_stale_year_range_in_license.py",
65+
True,
66+
["--use-current-year"],
67+
),
68+
),
5869
),
59-
)),
70+
),
6071
)
6172
def test_insert_license(license_file_path,
6273
src_file_path,
6374
comment_prefix,
6475
new_src_file_expected,
76+
message_expected,
6577
fail_check,
6678
extra_args,
6779
tmpdir):
6880
encoding = 'ISO-8859-1' if 'iso8859' in src_file_path else 'utf-8'
6981
with chdir_to_test_resources():
70-
path = tmpdir.join('src_file_path')
82+
path = tmpdir.join(src_file_path)
7183
shutil.copy(src_file_path, path.strpath)
7284
args = ['--license-filepath', license_file_path, '--comment-style', comment_prefix, path.strpath]
7385
if extra_args is not None:
7486
args.extend(extra_args)
75-
assert insert_license(args) == (1 if fail_check else 0)
87+
88+
with capture_stdout() as stdout:
89+
assert insert_license(args) == (1 if fail_check else 0)
90+
assert message_expected in stdout.getvalue()
91+
7692
if new_src_file_expected:
7793
with open(new_src_file_expected, encoding=encoding) as expected_content_file:
7894
expected_content = expected_content_file.read()
@@ -82,6 +98,33 @@ def test_insert_license(license_file_path,
8298
assert new_file_content == expected_content
8399

84100

101+
@pytest.mark.parametrize(
102+
('license_file_path', 'src_file_path', 'comment_prefix'),
103+
map(lambda a: a[:1] + a[1], product( # combine license files with other args
104+
('LICENSE_with_trailing_newline.txt', 'LICENSE_without_trailing_newline.txt'),
105+
(
106+
('module_with_license.groovy', '//'),
107+
('module_with_license_and_numbers.py', '#'),
108+
('module_with_year_range_in_license.py', '#'),
109+
('module_with_spaced_year_range_in_license.py', '#'),
110+
),
111+
)),
112+
)
113+
def test_insert_license_current_year_already_there(license_file_path,
114+
src_file_path,
115+
comment_prefix,
116+
tmpdir):
117+
with chdir_to_test_resources():
118+
with open(src_file_path, encoding="utf-8") as src_file:
119+
input_contents = src_file.read().replace("2017", str(datetime.now().year))
120+
path = tmpdir.join('src_file_path')
121+
with open(path.strpath, "w", encoding="utf-8") as input_file:
122+
input_file.write(input_contents)
123+
124+
args = ['--license-filepath', license_file_path, '--comment-style', comment_prefix, "--use-current-year", path.strpath]
125+
assert insert_license(args) == 0
126+
127+
85128
@pytest.mark.parametrize(
86129
('license_file_path', 'src_file_path', 'comment_style', 'new_src_file_expected', 'fail_check'),
87130
map(lambda a: a[:1] + a[1], product( # combine license files with other args
@@ -229,8 +272,18 @@ def test_remove_license(license_file_path,
229272
def chdir_to_test_resources():
230273
prev_dir = os.getcwd()
231274
try:
232-
res_dir = os.path.dirname(os.path.realpath(__file__)) +'/resources'
275+
res_dir = os.path.dirname(os.path.realpath(__file__)) + '/resources'
233276
os.chdir(res_dir)
234277
yield
235278
finally:
236279
os.chdir(prev_dir)
280+
281+
282+
@contextmanager
283+
def capture_stdout():
284+
try:
285+
captured = io.StringIO()
286+
sys.stdout = captured
287+
yield captured
288+
finally:
289+
sys.stdout = sys.__stdout__
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Copyright (C) 2016-2017 Teela O'Malley
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
5+
import sys
6+
sys.stdout.write("1985") # 1985
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Copyright (C) 2015 - 2017 Teela O'Malley
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
5+
import sys
6+
sys.stdout.write("FOO")

0 commit comments

Comments
 (0)