Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions polymath_code_standard/checkers/copyright.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pathlib import Path

from polymath_code_standard.checker import CheckerGroup, Result, check_group, filter_files
from polymath_code_standard.insert_license import COPYRIGHT_ORG_SENTINEL
from polymath_code_standard.licenses import PROPRIETARY, get_license_full_text, get_license_header


Expand All @@ -24,12 +25,23 @@ def register_args(self, subparser: argparse.ArgumentParser) -> None:
metavar='SPDX_ID',
help="SPDX license ID (e.g. MIT, Apache-2.0) or 'proprietary'",
)
subparser.add_argument(
org_group = subparser.add_mutually_exclusive_group(required=True)
org_group.add_argument(
'--copyright-org',
required=True,
default=None,
metavar='ORG',
help='Organization name for the copyright line',
)
org_group.add_argument(
'--wildcard-copyright-org',
action='store_true',
dest='wildcard_copyright_org',
help=(
'Accept any copyright holder on the copyright line (for multi-contributor repos). '
f'New headers are inserted with the sentinel "{COPYRIGHT_ORG_SENTINEL}" '
"which fails the check until replaced with the contributor's organization."
),
)
subparser.add_argument(
'--copyright-year',
default=str(datetime.date.today().year),
Expand All @@ -48,8 +60,9 @@ def register_args(self, subparser: argparse.ArgumentParser) -> None:
)

def run(self, args: argparse.Namespace) -> list[Result]:
insert_org = COPYRIGHT_ORG_SENTINEL if args.wildcard_copyright_org else args.copyright_org
header_text = get_license_header(
args.license_id, args.copyright_year, args.copyright_org, reuse_style_header=args.reuse_style
args.license_id, args.copyright_year, insert_org, reuse_style_header=args.reuse_style
)
py_cmake_shell = filter_files(args.files, frozenset({'python', 'cmake', 'shell'}))
cpp = filter_files(args.files, frozenset({'c', 'c++'}))
Expand All @@ -60,6 +73,7 @@ def run(self, args: argparse.Namespace) -> list[Result]:
for f in cpp:
self._strip_leading_comment_block(f, '//')

wildcard_flag = ['--wildcard-copyright-org'] if args.wildcard_copyright_org else []
fd, license_filepath = tempfile.mkstemp(suffix='.txt', prefix='polymath_license_')
try:
os.write(fd, header_text.encode('utf-8'))
Expand All @@ -74,13 +88,15 @@ def run(self, args: argparse.Namespace) -> list[Result]:
'#',
'--allow-past-years',
'--no-extra-eol',
],
]
+ wildcard_flag,
py_cmake_shell,
name='copyright (py/cmake/shell)',
),
self._check(
'polymath_copyright_header',
['--license-filepath', license_filepath, '--comment-style', '//', '--allow-past-years'],
['--license-filepath', license_filepath, '--comment-style', '//', '--allow-past-years']
+ wildcard_flag,
cpp,
name='copyright (cpp)',
),
Expand All @@ -91,7 +107,7 @@ def run(self, args: argparse.Namespace) -> list[Result]:
except OSError:
pass

results.append(self._check_license_file(args.license_id, args.copyright_year, args.copyright_org))
results.append(self._check_license_file(args.license_id, args.copyright_year, args.copyright_org or ''))
return results

@staticmethod
Expand Down
42 changes: 39 additions & 3 deletions polymath_code_standard/insert_license.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@

SKIP_LICENSE_INSERTION_COMMENT = 'SKIP LICENSE INSERTION'

COPYRIGHT_ORG_SENTINEL = 'SET_YOUR_ORGANIZATION_HERE'

DEBUG_LEVENSHTEIN_DISTANCE_CALCULATION = False

LicenseInfo = collections.namedtuple(
Expand Down Expand Up @@ -89,6 +91,16 @@ def main(argv=None) -> int:
help="Insert license after line matching regex (ex: '^<\\?php$')",
)
parser.add_argument('--remove-header', action='store_true')
parser.add_argument(
'--wildcard-copyright-org',
action='store_true',
dest='wildcard_copyright_org',
help=(
'Accept any copyright holder on the copyright line. '
f'New headers are inserted with the sentinel "{COPYRIGHT_ORG_SENTINEL}" '
'which fails the check until replaced.'
),
)
parser.add_argument(
'--use-current-year',
action='store_true',
Expand Down Expand Up @@ -194,6 +206,15 @@ def process_files(
top_lines_count=args.detect_license_in_X_top_lines,
):
continue
if args.wildcard_copyright_org and copyright_sentinel_found(
src_file_content, args.detect_license_in_X_top_lines
):
print(
f'{src_filepath}: copyright org placeholder "{COPYRIGHT_ORG_SENTINEL}" '
'not yet replaced — update the copyright line with your organization name'
)
license_update_failed = True
continue
if fail_license_todo_found(
src_file_content=src_file_content,
fuzzy_match_todo_comment=args.fuzzy_match_todo_comment,
Expand All @@ -210,6 +231,7 @@ def process_files(
license_info=license_info,
top_lines_count=args.detect_license_in_X_top_lines,
match_years_strictly=not args.allow_past_years,
wildcard_copyright_org=args.wildcard_copyright_org,
)
if license_header_index is not None:
break
Expand Down Expand Up @@ -421,22 +443,24 @@ def _strip_years(line):
return _YEARS_PATTERN.sub('', line)


def _license_line_matches(license_line, src_file_line, match_years_strictly):
def _license_line_matches(license_line, src_file_line, match_years_strictly, wildcard_copyright_org=False):
license_line = license_line.strip()
src_file_line = src_file_line.strip()
if wildcard_copyright_org and _is_copyright_line(license_line):
return _is_copyright_line(src_file_line)
if match_years_strictly:
return license_line == src_file_line
return _strip_years(license_line) == _strip_years(src_file_line)


def find_license_header_index(
src_file_content, license_info: LicenseInfo, top_lines_count, match_years_strictly
src_file_content, license_info: LicenseInfo, top_lines_count, match_years_strictly, wildcard_copyright_org=False
) -> int | None:
for i in range(top_lines_count):
license_match = True
for j, license_line in enumerate(license_info.prefixed_license):
if i + j >= len(src_file_content) or not _license_line_matches(
license_line, src_file_content[i + j], match_years_strictly
license_line, src_file_content[i + j], match_years_strictly, wildcard_copyright_org
):
license_match = False
break
Expand All @@ -445,6 +469,18 @@ def find_license_header_index(
return None


def _is_copyright_line(stripped_line: str) -> bool:
"""Return True if a stripped line is a copyright attribution (any comment style)."""
return stripped_line.lstrip('/#').lstrip().lower().startswith('copyright')


def copyright_sentinel_found(src_file_content, top_lines_count):
for i in range(min(top_lines_count, len(src_file_content))):
if COPYRIGHT_ORG_SENTINEL in src_file_content[i]:
return True
return False


def skip_license_insert_found(src_file_content, skip_license_insertion_comment, top_lines_count):
for i in range(top_lines_count):
if i < len(src_file_content) and skip_license_insertion_comment in src_file_content[i]:
Expand Down
Loading
Loading