From 0a8777648a43c289edcf7ae488df4d6a57f6c2f1 Mon Sep 17 00:00:00 2001 From: Leszek Lugin Date: Wed, 10 Jul 2024 16:07:42 +0200 Subject: [PATCH 1/6] Fix handle IndexError exception --- pontos/updateheader/updateheader.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pontos/updateheader/updateheader.py b/pontos/updateheader/updateheader.py index c03c3c725..e6e6843ab 100644 --- a/pontos/updateheader/updateheader.py +++ b/pontos/updateheader/updateheader.py @@ -67,7 +67,12 @@ def _get_modified_year(f: Path) -> str: """In case of the changed arg, update year to last modified year""" - return Git().log("-1", "--date=format:%Y", str(f), format="%ad")[0] + try: + ret = Git().log("-1", "--date=format:%Y", str(f), format="%ad")[0] + except IndexError: + raise PontosError(f"Empty \"git log -1\" output for {f}.") + + return ret @dataclass From 1c074d2fc3809c9df24dcb6906af55fd61ef9fdc Mon Sep 17 00:00:00 2001 From: Leszek Lugin Date: Wed, 10 Jul 2024 16:39:15 +0200 Subject: [PATCH 2/6] Change check for excluded first when updating a file No point for calling _get_modified_year() for excluded files. --- pontos/updateheader/updateheader.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pontos/updateheader/updateheader.py b/pontos/updateheader/updateheader.py index e6e6843ab..c132405fb 100644 --- a/pontos/updateheader/updateheader.py +++ b/pontos/updateheader/updateheader.py @@ -350,19 +350,20 @@ def main(args: Optional[Sequence[str]] = None) -> None: sys.exit(1) for file in files: - if changed: - try: - year = _get_modified_year(file) - except PontosError: - term.warning( - f"{file}: Could not get date of last modification" - f" via git, using {year} instead." - ) try: if file.absolute() in exclude_list: term.warning(f"{file}: Ignoring file from exclusion list.") else: + if changed: + try: + year = _get_modified_year(file) + except PontosError: + term.warning( + f"{file}: Could not get date of last modification" + f" via git, using {year} instead." + ) + update_file( file, year, From 9e73e04e9fce7050f065efd484f49163347c9ed9 Mon Sep 17 00:00:00 2001 From: Leszek Lugin Date: Wed, 10 Jul 2024 16:43:13 +0200 Subject: [PATCH 3/6] Add: single-year flag to command pontos-update-header To enable using only creation year for header license. --- pontos/updateheader/_parser.py | 11 ++++++++ pontos/updateheader/updateheader.py | 9 +++++- tests/updateheader/test_header.py | 44 +++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/pontos/updateheader/_parser.py b/pontos/updateheader/_parser.py index a14bc5869..7315ae646 100644 --- a/pontos/updateheader/_parser.py +++ b/pontos/updateheader/_parser.py @@ -109,4 +109,15 @@ def parse_args(args: Optional[Sequence[str]] = None) -> Namespace: help="Do a cleanup: Remove lines from outdated header format", ) + parser.add_argument( + "--single-year", + action="store_true", + default=False, + help=( + "If set, will not update license with from-to years format " + "(YYYY-YYYY) if it has single (only creation) year format (YYYY). " + "Default is %(default)s." + ), + ) + return parser.parse_args(args) diff --git a/pontos/updateheader/updateheader.py b/pontos/updateheader/updateheader.py index c132405fb..5339436c9 100644 --- a/pontos/updateheader/updateheader.py +++ b/pontos/updateheader/updateheader.py @@ -70,7 +70,7 @@ def _get_modified_year(f: Path) -> str: try: ret = Git().log("-1", "--date=format:%Y", str(f), format="%ad")[0] except IndexError: - raise PontosError(f"Empty \"git log -1\" output for {f}.") + raise PontosError(f'Empty "git log -1" output for {f}.') return ret @@ -154,6 +154,7 @@ def update_file( company: str, *, cleanup: bool = False, + single_year: bool = False, ) -> None: """Function to update the header of the given file @@ -213,6 +214,10 @@ def update_file( or copyright_match.modification_year and copyright_match.modification_year < year ): + if single_year: + # In case of single year updating the license with modification date doesn't make sense. + # Changing the existing license header with created-modified years to single year is not supported. + return copyright_term = ( f"SPDX-FileCopyrightText: " f"{copyright_match.creation_year}" @@ -314,6 +319,7 @@ def main(args: Optional[Sequence[str]] = None) -> None: changed: bool = parsed_args.changed quiet: bool = parsed_args.quiet cleanup: bool = parsed_args.cleanup + single_year: bool = parsed_args.single_year if quiet: term: Union[NullTerminal, RichTerminal] = NullTerminal() @@ -370,6 +376,7 @@ def main(args: Optional[Sequence[str]] = None) -> None: license_id, company, cleanup=cleanup, + single_year=single_year, ) except (FileNotFoundError, UnicodeDecodeError, ValueError): continue diff --git a/tests/updateheader/test_header.py b/tests/updateheader/test_header.py index 560fda065..e00b48bb4 100644 --- a/tests/updateheader/test_header.py +++ b/tests/updateheader/test_header.py @@ -291,6 +291,26 @@ def test_update_create_header(self, mock_stdout): expected_header, test_file.read_text(encoding="utf-8") ) + @patch("sys.stdout", new_callable=StringIO) + def test_update_create_header_single_year(self, mock_stdout): + year = "1995" + license_id = "AGPL-3.0-or-later" + + expected_header = HEADER.format(date="1995") + "\n\n" + + with temp_file(name="test.py", change_into=True) as test_file: + update_file( + test_file, year, license_id, self.company, single_year=True + ) + ret = mock_stdout.getvalue() + self.assertEqual( + f"{test_file}: Added license header.\n", + ret, + ) + self.assertEqual( + expected_header, test_file.read_text(encoding="utf-8") + ) + @patch("sys.stdout", new_callable=StringIO) def test_update_header_in_file(self, mock_stdout): year = "2021" @@ -318,6 +338,28 @@ def test_update_header_in_file(self, mock_stdout): test_file.read_text(encoding="utf-8"), ) + @patch("sys.stdout", new_callable=StringIO) + def test_update_header_in_file_single_year(self, mock_stdout): + year = "2021" + license_id = "AGPL-3.0-or-later" + + header = HEADER.format(date="2020") + with temp_file( + content=header, name="test.py", change_into=True + ) as test_file: + update_file( + test_file, + year, + license_id, + self.company, + single_year=True, + ) + + self.assertIn( + "# SPDX-FileCopyrightText: 2020 Greenbone AG", + test_file.read_text(encoding="utf-8"), + ) + @patch("sys.stdout", new_callable=StringIO) def test_update_header_ok_in_file(self, mock_stdout): year = "2021" @@ -510,6 +552,7 @@ def test_defaults(self): self.assertEqual(args.files, ["foo.txt"]) self.assertIsNone(args.directories) self.assertIsNone(args.exclude_file) + self.assertFalse(args.single_year) self.assertFalse(args.cleanup) def test_files_and_directories_mutual_exclusive(self): @@ -569,6 +612,7 @@ def test_main_never_happen(self, argparser_mock, mock_stdout): self.args.log_file = None self.args.quiet = False self.args.cleanup = False + self.args.single_year = False argparser_mock.return_value = self.args From 5beb3cdf45cf215bdfe73e0f543ea9c394193187 Mon Sep 17 00:00:00 2001 From: Leszek Lugin Date: Fri, 9 Aug 2024 15:13:18 +0200 Subject: [PATCH 4/6] Enable updating existing multi-year license headers with single_year --- pontos/updateheader/_parser.py | 4 +- pontos/updateheader/updateheader.py | 86 ++++++++++++++++++----------- tests/updateheader/test_header.py | 9 ++- 3 files changed, 65 insertions(+), 34 deletions(-) diff --git a/pontos/updateheader/_parser.py b/pontos/updateheader/_parser.py index 7315ae646..577f5d0c2 100644 --- a/pontos/updateheader/_parser.py +++ b/pontos/updateheader/_parser.py @@ -114,8 +114,8 @@ def parse_args(args: Optional[Sequence[str]] = None) -> Namespace: action="store_true", default=False, help=( - "If set, will not update license with from-to years format " - "(YYYY-YYYY) if it has single (only creation) year format (YYYY). " + "If set, will format license headers in from-to year format " + "into single (creation) year format. " "Default is %(default)s." ), ) diff --git a/pontos/updateheader/updateheader.py b/pontos/updateheader/updateheader.py index 5339436c9..9f70ede94 100644 --- a/pontos/updateheader/updateheader.py +++ b/pontos/updateheader/updateheader.py @@ -8,6 +8,7 @@ Also it appends a header if it is missing in the file. """ +import io import re import sys from dataclasses import dataclass @@ -208,39 +209,43 @@ def update_file( return # replace found header and write it to file - if copyright_match and ( - not copyright_match.modification_year - and copyright_match.creation_year < year - or copyright_match.modification_year - and copyright_match.modification_year < year - ): + if copyright_match: + + # use different target license formats depending on provided single_year argument if single_year: - # In case of single year updating the license with modification date doesn't make sense. - # Changing the existing license header with created-modified years to single year is not supported. - return - copyright_term = ( - f"SPDX-FileCopyrightText: " - f"{copyright_match.creation_year}" - f"-{year} {company}" - ) - new_line = re.sub(copyright_regex, copyright_term, line) - fp_write = fp.tell() - len(line) # save position to insert - rest_of_file = fp.read() - fp.seek(fp_write) - fp.write(new_line) - fp.write(rest_of_file) - # in some cases we replace "YYYY - YYYY" with "YYYY-YYYY" - # resulting in 2 characters left at the end of the file - # so we truncate the file, just in case! - fp.truncate() - print( - f"{file}: Changed License Header Copyright Year " - f"{copyright_match.modification_year} -> " - f"{year}" - ) + copyright_term = ( + f"SPDX-FileCopyrightText: " + f"{copyright_match.creation_year} " + f"{company}" + ) + else: + copyright_term = ( + f"SPDX-FileCopyrightText: " + f"{copyright_match.creation_year}" + f"-{year} {company}" + ) + + with_multi_year = copyright_match.creation_year and copyright_match.modification_year + with_single_year_outdated = not copyright_match.modification_year and copyright_match.creation_year < year + with_multi_year_outdated = with_multi_year and copyright_match.modification_year < year + + if single_year and with_multi_year: + _substitute_license_text(fp, line, copyright_regex, copyright_term) + print( + f"{file}: Changed License Header Copyright Year format to single year " + f"{copyright_match.creation_year}-{year} -> " + f"{copyright_match.creation_year}" + ) + elif not single_year and (with_multi_year_outdated or with_single_year_outdated): + _substitute_license_text(fp, line, copyright_regex, copyright_term) + print( + f"{file}: Changed License Header Copyright Year " + f"{copyright_match.modification_year} -> " + f"{year}" + ) + else: + print(f"{file}: License Header is ok.") - else: - print(f"{file}: License Header is ok.") except FileNotFoundError as e: print(f"{file}: File is not existing.") raise e @@ -258,6 +263,25 @@ def update_file( print(f"{file}: Cleaned up!") +def _substitute_license_text( + fp: io.TextIOWrapper, + line: str, + copyright_regex: re.Pattern, + copyright_term: str, +) -> None: + """Substitute the old license text in file fp, starting on provided line, with the new one provided in copyright_term""" + new_line = re.sub(copyright_regex, copyright_term, line) + fp_write = fp.tell() - len(line) # save position to insert + rest_of_file = fp.read() + fp.seek(fp_write) + fp.write(new_line) + fp.write(rest_of_file) + # in some cases we replace "YYYY - YYYY" with "YYYY-YYYY" + # resulting in 2 characters left at the end of the file + # so we truncate the file, just in case! + fp.truncate() + + def _get_exclude_list( exclude_file: Path, directories: list[Path] ) -> list[Path]: diff --git a/tests/updateheader/test_header.py b/tests/updateheader/test_header.py index e00b48bb4..59dfe8e7b 100644 --- a/tests/updateheader/test_header.py +++ b/tests/updateheader/test_header.py @@ -343,7 +343,7 @@ def test_update_header_in_file_single_year(self, mock_stdout): year = "2021" license_id = "AGPL-3.0-or-later" - header = HEADER.format(date="2020") + header = HEADER.format(date="2020-2021") with temp_file( content=header, name="test.py", change_into=True ) as test_file: @@ -355,6 +355,13 @@ def test_update_header_in_file_single_year(self, mock_stdout): single_year=True, ) + ret = mock_stdout.getvalue() + self.assertEqual( + ret, + f"{test_file}: Changed License Header Copyright Year format to single year " + "2020-2021 -> 2020\n", + ) + self.assertIn( "# SPDX-FileCopyrightText: 2020 Greenbone AG", test_file.read_text(encoding="utf-8"), From f60153c8f4e3418e42f4cdce2df3a518c0411cac Mon Sep 17 00:00:00 2001 From: Leszek Lugin Date: Fri, 9 Aug 2024 16:53:06 +0200 Subject: [PATCH 5/6] Compare years as ints --- pontos/updateheader/updateheader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pontos/updateheader/updateheader.py b/pontos/updateheader/updateheader.py index 9f70ede94..9d76d5c3b 100644 --- a/pontos/updateheader/updateheader.py +++ b/pontos/updateheader/updateheader.py @@ -226,8 +226,8 @@ def update_file( ) with_multi_year = copyright_match.creation_year and copyright_match.modification_year - with_single_year_outdated = not copyright_match.modification_year and copyright_match.creation_year < year - with_multi_year_outdated = with_multi_year and copyright_match.modification_year < year + with_single_year_outdated = not copyright_match.modification_year and int(copyright_match.creation_year) < int(year) + with_multi_year_outdated = with_multi_year and int(copyright_match.modification_year) < int(year) if single_year and with_multi_year: _substitute_license_text(fp, line, copyright_regex, copyright_term) From df21d08b86e81ddd0fff00805708cbab2eea61ca Mon Sep 17 00:00:00 2001 From: Leszek Lugin Date: Fri, 9 Aug 2024 16:59:32 +0200 Subject: [PATCH 6/6] Apply linter formatting --- pontos/updateheader/updateheader.py | 31 +++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/pontos/updateheader/updateheader.py b/pontos/updateheader/updateheader.py index 9d76d5c3b..de45324b2 100644 --- a/pontos/updateheader/updateheader.py +++ b/pontos/updateheader/updateheader.py @@ -225,19 +225,38 @@ def update_file( f"-{year} {company}" ) - with_multi_year = copyright_match.creation_year and copyright_match.modification_year - with_single_year_outdated = not copyright_match.modification_year and int(copyright_match.creation_year) < int(year) - with_multi_year_outdated = with_multi_year and int(copyright_match.modification_year) < int(year) + with_multi_year = ( + copyright_match.creation_year + and copyright_match.modification_year + ) + with_single_year_outdated = ( + not copyright_match.modification_year + and int(copyright_match.creation_year) < int(year) + ) + + with_multi_year_outdated = False + if with_multi_year: + # assert to silence mypy + assert isinstance(copyright_match.modification_year, str) + with_multi_year_outdated = int( + copyright_match.modification_year + ) < int(year) if single_year and with_multi_year: - _substitute_license_text(fp, line, copyright_regex, copyright_term) + _substitute_license_text( + fp, line, copyright_regex, copyright_term + ) print( f"{file}: Changed License Header Copyright Year format to single year " f"{copyright_match.creation_year}-{year} -> " f"{copyright_match.creation_year}" ) - elif not single_year and (with_multi_year_outdated or with_single_year_outdated): - _substitute_license_text(fp, line, copyright_regex, copyright_term) + elif not single_year and ( + with_multi_year_outdated or with_single_year_outdated + ): + _substitute_license_text( + fp, line, copyright_regex, copyright_term + ) print( f"{file}: Changed License Header Copyright Year " f"{copyright_match.modification_year} -> "