Skip to content

Commit 6fa35f2

Browse files
authored
Allow for some variation in the license header line (#7)
* Allow for some variation in the license header line, matching some existing styles that are acceptable but not exactly what the formatter would insert Signed-off-by: Emerson Knapp <emerson@polymathrobotics.com>
1 parent c148d49 commit 6fa35f2

4 files changed

Lines changed: 175 additions & 2 deletions

File tree

polymath_code_standard/insert_license.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,10 @@ def process_files(
262262
except LicenseUpdateError as error:
263263
print(error)
264264
license_update_failed = True
265+
elif args.wildcard_copyright_org and any_copyright_line_found(
266+
src_file_content, args.detect_license_in_X_top_lines
267+
):
268+
pass # existing attribution in a different format — leave the file untouched
265269
else:
266270
if fuzzy_match_header_index is not None:
267271
if fuzzy_license_found(
@@ -437,17 +441,31 @@ def fuzzy_license_found(
437441

438442

439443
_YEARS_PATTERN = re.compile(r'\b\d{4}([ ,-]+\d{2,4})*\b')
444+
_COPYRIGHT_C_PATTERN = re.compile(r'\(c\)\s*', re.IGNORECASE)
445+
_YEAR_PRESENT_PATTERN = re.compile(r'(\b\d{4})-present\b', re.IGNORECASE)
446+
_ALL_RIGHTS_RESERVED_PATTERN = re.compile(r'[.,]?\s*\ball rights reserved\.?\s*', re.IGNORECASE)
440447

441448

442449
def _strip_years(line):
443450
return _YEARS_PATTERN.sub('', line)
444451

445452

453+
def _normalize_copyright_line(line: str) -> str:
454+
"""Strip cosmetic decorators from a copyright line for loose comparison."""
455+
line = _COPYRIGHT_C_PATTERN.sub('', line)
456+
line = _YEAR_PRESENT_PATTERN.sub(r'\1', line)
457+
line = _ALL_RIGHTS_RESERVED_PATTERN.sub('', line)
458+
return ' '.join(line.split())
459+
460+
446461
def _license_line_matches(license_line, src_file_line, match_years_strictly, wildcard_copyright_org=False):
447462
license_line = license_line.strip()
448463
src_file_line = src_file_line.strip()
449464
if wildcard_copyright_org and _is_copyright_line(license_line):
450465
return _is_copyright_line(src_file_line)
466+
if _is_copyright_line(license_line):
467+
license_line = _normalize_copyright_line(license_line)
468+
src_file_line = _normalize_copyright_line(src_file_line)
451469
if match_years_strictly:
452470
return license_line == src_file_line
453471
return _strip_years(license_line) == _strip_years(src_file_line)
@@ -482,6 +500,13 @@ def copyright_sentinel_found(src_file_content, top_lines_count):
482500
return False
483501

484502

503+
def any_copyright_line_found(src_file_content, top_lines_count):
504+
for i in range(min(top_lines_count, len(src_file_content))):
505+
if _is_copyright_line(src_file_content[i].strip()):
506+
return True
507+
return False
508+
509+
485510
def skip_license_insert_found(src_file_content, skip_license_insertion_comment, top_lines_count):
486511
for i in range(top_lines_count):
487512
if i < len(src_file_content) and skip_license_insertion_comment in src_file_content[i]:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "polymath-code-standard"
7-
version = "2.0.1"
7+
version = "2.1.2"
88
description = "Polymath Code Standard pre-commit hooks"
99
requires-python = ">=3.10"
1010
dependencies = [

tests/test_insert_license_wildcard.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
LicenseInfo,
88
_is_copyright_line,
99
_license_line_matches,
10+
_normalize_copyright_line,
11+
any_copyright_line_found,
1012
copyright_sentinel_found,
1113
find_license_header_index,
1214
main,
@@ -80,6 +82,76 @@ def test_reuse_spdx_license_identifier_is_not_copyright(self):
8082
assert not _is_copyright_line('# SPDX-License-Identifier: Apache-2.0')
8183

8284

85+
# ---------------------------------------------------------------------------
86+
# _normalize_copyright_line
87+
# ---------------------------------------------------------------------------
88+
89+
90+
class TestNormalizeCopyrightLine:
91+
def test_strips_c_symbol(self):
92+
assert _normalize_copyright_line('# Copyright (c) 2026 Acme Corp') == '# Copyright 2026 Acme Corp'
93+
94+
def test_strips_c_symbol_uppercase(self):
95+
assert _normalize_copyright_line('# Copyright (C) 2026 Acme Corp') == '# Copyright 2026 Acme Corp'
96+
97+
def test_strips_present_suffix(self):
98+
assert _normalize_copyright_line('# Copyright 2025-present Acme Corp') == '# Copyright 2025 Acme Corp'
99+
100+
def test_strips_all_rights_reserved(self):
101+
assert (
102+
_normalize_copyright_line('# Copyright 2026 Acme Corp. All rights reserved') == '# Copyright 2026 Acme Corp'
103+
)
104+
105+
def test_strips_all_combined(self):
106+
result = _normalize_copyright_line('// Copyright (c) 2025-present Acme Corp. All rights reserved')
107+
assert result == '// Copyright 2025 Acme Corp'
108+
109+
def test_leaves_plain_line_unchanged(self):
110+
assert _normalize_copyright_line('# Copyright 2026 Acme Corp') == '# Copyright 2026 Acme Corp'
111+
112+
113+
# ---------------------------------------------------------------------------
114+
# _license_line_matches — copyright decoration tolerance (no wildcard needed)
115+
# ---------------------------------------------------------------------------
116+
117+
118+
class TestLicenseLineMatchesDecoration:
119+
def test_c_symbol_matches_plain_template(self):
120+
assert _license_line_matches(
121+
'// Copyright 2026 Acme Corp',
122+
'// Copyright (c) 2026 Acme Corp',
123+
match_years_strictly=False,
124+
)
125+
126+
def test_present_suffix_matches_plain_template(self):
127+
assert _license_line_matches(
128+
'// Copyright 2026 Acme Corp',
129+
'// Copyright 2025-present Acme Corp',
130+
match_years_strictly=False,
131+
)
132+
133+
def test_all_rights_reserved_matches_plain_template(self):
134+
assert _license_line_matches(
135+
'// Copyright 2026 Acme Corp',
136+
'// Copyright 2026 Acme Corp. All rights reserved',
137+
match_years_strictly=False,
138+
)
139+
140+
def test_all_decorations_combined(self):
141+
assert _license_line_matches(
142+
'// Copyright 2026 Acme Corp',
143+
'// Copyright (c) 2025-present Acme Corp. All rights reserved',
144+
match_years_strictly=False,
145+
)
146+
147+
def test_wrong_org_still_fails(self):
148+
assert not _license_line_matches(
149+
'// Copyright 2026 Acme Corp',
150+
'// Copyright (c) 2025-present Other Corp. All rights reserved',
151+
match_years_strictly=False,
152+
)
153+
154+
83155
# ---------------------------------------------------------------------------
84156
# _license_line_matches — wildcard_copyright_org
85157
# ---------------------------------------------------------------------------
@@ -138,6 +210,33 @@ def test_sentinel_beyond_top_lines_not_detected(self):
138210
assert not copyright_sentinel_found(content, top_lines_count=2)
139211

140212

213+
# ---------------------------------------------------------------------------
214+
# any_copyright_line_found
215+
# ---------------------------------------------------------------------------
216+
217+
218+
class TestAnyCopyrightLineFound:
219+
def test_finds_standard_copyright(self):
220+
content = ['# Copyright (c) 2025-present Acme Corp. All rights reserved\n', 'import foo\n']
221+
assert any_copyright_line_found(content, top_lines_count=5)
222+
223+
def test_finds_cpp_style(self):
224+
content = ['// Copyright (c) 2025-present Acme Corp. All rights reserved\n', 'int x;\n']
225+
assert any_copyright_line_found(content, top_lines_count=5)
226+
227+
def test_not_found_in_plain_code(self):
228+
content = ['import foo\n', 'x = 1\n']
229+
assert not any_copyright_line_found(content, top_lines_count=5)
230+
231+
def test_sentinel_line_also_counts(self):
232+
content = [f'# Copyright 2026 {COPYRIGHT_ORG_SENTINEL}\n']
233+
assert any_copyright_line_found(content, top_lines_count=5)
234+
235+
def test_beyond_top_lines_not_found(self):
236+
content = ['import foo\n', 'import bar\n', '# Copyright 2026 Acme\n']
237+
assert not any_copyright_line_found(content, top_lines_count=2)
238+
239+
141240
# ---------------------------------------------------------------------------
142241
# find_license_header_index — wildcard matching
143242
# ---------------------------------------------------------------------------
@@ -280,3 +379,52 @@ def test_without_wildcard_rejects_different_org(self, tmp_path):
280379
])
281380
assert ret == 1
282381
assert src.read_text() != original
382+
383+
def test_single_line_copyright_passes_with_wildcard(self, tmp_path):
384+
# Existing repos often have a condensed copyright line without the full license block
385+
content = '// Copyright (c) 2025-present Polymath Robotics, Inc. All rights reserved\nint x = 0;\n'
386+
src = self._write(tmp_path, 'f.cpp', content)
387+
lf = self._license_file(tmp_path)
388+
ret = main([
389+
'--license-filepath',
390+
lf,
391+
'--comment-style',
392+
'//',
393+
'--allow-past-years',
394+
'--wildcard-copyright-org',
395+
str(src),
396+
])
397+
assert ret == 0
398+
assert src.read_text() == content # file untouched
399+
400+
def test_single_line_copyright_with_sentinel_still_fails(self, tmp_path):
401+
content = f'// Copyright (c) 2025-present {COPYRIGHT_ORG_SENTINEL}\nint x = 0;\n'
402+
src = self._write(tmp_path, 'f.cpp', content)
403+
lf = self._license_file(tmp_path)
404+
ret = main([
405+
'--license-filepath',
406+
lf,
407+
'--comment-style',
408+
'//',
409+
'--allow-past-years',
410+
'--wildcard-copyright-org',
411+
str(src),
412+
])
413+
assert ret == 1
414+
assert src.read_text() == content # file still untouched (sentinel check doesn't modify)
415+
416+
def test_no_copyright_at_all_inserts_sentinel(self, tmp_path):
417+
content = 'int x = 0;\n'
418+
src = self._write(tmp_path, 'f.cpp', content)
419+
lf = self._license_file(tmp_path)
420+
ret = main([
421+
'--license-filepath',
422+
lf,
423+
'--comment-style',
424+
'//',
425+
'--allow-past-years',
426+
'--wildcard-copyright-org',
427+
str(src),
428+
])
429+
assert ret == 1
430+
assert COPYRIGHT_ORG_SENTINEL in src.read_text()

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)