Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update header #987

Merged
merged 19 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
bf291e1
Add: Allow to create a binary file with temp_file context manager
bjoernricks Feb 22, 2024
e6b5161
Change: Split and improve update header test cases
bjoernricks Feb 22, 2024
96ff851
Change: Refactor update_header into a standalone function
bjoernricks Feb 23, 2024
c1a719e
Use Git class in _get_modified_year
bjoernricks Feb 23, 2024
da9ca91
Add: Add a testcase for parsing defaults for update header CLI
bjoernricks Feb 23, 2024
747a728
Further tests cleanup
bjoernricks Feb 23, 2024
10d0904
Change: Remove copyright_regex from update_header function
bjoernricks Feb 23, 2024
e733357
Simplify _get_exclude_list
bjoernricks Feb 23, 2024
0fce4f8
Use list comprehension for _compile_outdated_regex
bjoernricks Feb 23, 2024
d38e2a9
Use list and tuple instead of List and Tuple
bjoernricks Feb 23, 2024
06a9ba7
Remove obsolete Terminal mock class in update header test module
bjoernricks Feb 23, 2024
73d60cb
Change: Improve and finalize update_file function API
bjoernricks Feb 23, 2024
2239482
Add: Add test for update-header passing files and directories
bjoernricks Feb 23, 2024
02875d6
Change: Improve help for update-header arguments
bjoernricks Feb 23, 2024
f00c19d
Change: Allow to pass --year and --changed for update-header CLI
bjoernricks Feb 23, 2024
e9101b5
Easier testing for update-header main function
bjoernricks Feb 23, 2024
614c2da
Add a test for updating a header with spdx copyright
bjoernricks Feb 23, 2024
4bcbe9f
Fix: Fix overriding company in copyright header via update-header CLI
bjoernricks Feb 23, 2024
2a394c3
Only compile regex pattern once
bjoernricks Feb 26, 2024
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
17 changes: 14 additions & 3 deletions pontos/testing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@
import tempfile
from contextlib import contextmanager
from pathlib import Path
from typing import Any, AsyncIterator, Awaitable, Generator, Iterable, Optional
from typing import (
Any,
AsyncIterator,
Awaitable,
Generator,
Iterable,
Optional,
Union,
)

from pontos.git._git import exec_git
from pontos.helper import add_sys_path, ensure_unload_module, unload_module
Expand Down Expand Up @@ -136,7 +144,7 @@ def temp_git_repository(

@contextmanager
def temp_file(
content: Optional[str] = None,
content: Optional[Union[str, bytes]] = None,
*,
name: str = "test.toml",
change_into: bool = False,
Expand Down Expand Up @@ -166,7 +174,10 @@ def temp_file(
with temp_directory(change_into=change_into) as tmp_dir:
test_file = tmp_dir / name
if content:
test_file.write_text(content, encoding="utf8")
if isinstance(content, bytes):
test_file.write_bytes(content)
else:
test_file.write_text(content, encoding="utf8")
else:
test_file.touch()

Expand Down
14 changes: 7 additions & 7 deletions pontos/updateheader/_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,24 +39,24 @@ def parse_args(args: Optional[Sequence[str]] = None) -> Namespace:
help="Activate logging using the given file path",
).complete = shtab.FILE # type: ignore[attr-defined]

date_group = parser.add_mutually_exclusive_group()
date_group.add_argument(
parser.add_argument(
"-c",
"--changed",
action="store_true",
default=False,
help=(
"Update modified year using git log modified year. "
"This will not changed all files to current year!"
"Used instead of --year. If the modified year could not be "
"determined via git it falls back to --year."
),
)
date_group.add_argument(
parser.add_argument(
"-y",
"--year",
default=str(datetime.now().year),
help=(
"If year is set, modified year will be "
"set to the specified year."
"set to the specified year. Default is %(default)s."
),
)

Expand All @@ -66,15 +66,15 @@ def parse_args(args: Optional[Sequence[str]] = None) -> Namespace:
dest="license_id",
choices=SUPPORTED_LICENSES,
default="GPL-3.0-or-later",
help=("Use the passed license type"),
help="Use the passed license type. Default is %(default)s",
)

parser.add_argument(
"--company",
default="Greenbone AG",
help=(
"If a header will be added to file, "
"it will be licensed by company."
"it will be licensed by company. Default is %(default)s"
),
)

Expand Down
176 changes: 86 additions & 90 deletions pontos/updateheader/updateheader.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@

import re
import sys
from argparse import Namespace
from dataclasses import dataclass
from functools import cache
from pathlib import Path
from subprocess import CalledProcessError, run
from typing import Dict, List, Optional, Tuple, Union
from typing import Optional, Sequence, Union

from pontos.terminal import Terminal
from pontos.errors import PontosError
from pontos.git import Git
from pontos.terminal.null import NullTerminal
from pontos.terminal.rich import RichTerminal

Expand Down Expand Up @@ -60,39 +61,34 @@
"along with this program; if not, write to the Free Software",
"Foundation, Inc\., 51 Franklin St, Fifth Floor, Boston, MA 02110\-1301 USA\.", # noqa: E501
]
OLD_COMPANY = "Greenbone Networks GmbH"


def _get_modified_year(f: Path) -> str:
"""In case of the changed arg, update year to last modified year"""
try:
cmd = ["git", "log", "-1", "--format=%ad", "--date=format:%Y", str(f)]
proc = run(
cmd,
text=True,
capture_output=True,
check=True,
universal_newlines=True,
)
return proc.stdout.rstrip()
except CalledProcessError as e:
raise e
return Git().log("-1", "--date=format:%Y", str(f), format="%ad")[0]


@dataclass
class CopyrightMatch:
creation_year: str
modification_year: Optional[str]
company: str


def _find_copyright(
line: str,
copyright_regex: re.Pattern,
) -> Tuple[bool, Union[Dict[str, Union[str, None]], None]]:
) -> tuple[bool, Union[CopyrightMatch, None]]:
"""Match the line for the copyright_regex"""
copyright_match = re.search(copyright_regex, line)
if copyright_match:
return (
True,
{
"creation_year": copyright_match.group(2),
"modification_year": copyright_match.group(3),
"company": copyright_match.group(4),
},
CopyrightMatch(
creation_year=copyright_match.group(2),
modification_year=copyright_match.group(3),
company=copyright_match.group(4),
),
)
return False, None

Expand Down Expand Up @@ -121,7 +117,7 @@ def _add_header(


def _remove_outdated_lines(
content: str, cleanup_regexes: List[re.Pattern]
content: str, cleanup_regexes: list[re.Pattern]
) -> Optional[str]:
"""Remove lines that contain outdated copyright header ..."""
changed = False
Expand All @@ -144,27 +140,22 @@ def _remove_outdated_lines(
return None


def _update_file(
def update_file(
file: Path,
copyright_regex: re.Pattern,
parsed_args: Namespace,
term: Terminal,
cleanup_regexes: Optional[List[re.Pattern]] = None,
) -> int:
"""Function to update the given file.
Checks if header exists. If not it adds an
header to that file, else it checks if year
is up to date
year: str,
license_id: str,
company: str,
*,
cleanup: bool = False,
) -> None:
"""Function to update the header of the given file

Checks if header exists. If not it adds an header to that file, otherwise it
checks if year is up to date
"""

if parsed_args.changed:
try:
parsed_args.year = _get_modified_year(file)
except CalledProcessError:
term.warning(
f"{file}: Could not get date of last modification"
f" using git, using {str(parsed_args.year)} instead."
)
copyright_regex = _compile_copyright_regex()
cleanup_regexes = _compile_outdated_regex() if cleanup else None
bjoernricks marked this conversation as resolved.
Show resolved Hide resolved

try:
with file.open("r+") as fp:
Expand All @@ -184,39 +175,41 @@ def _update_file(
try:
header = _add_header(
file.suffix,
parsed_args.license_id,
parsed_args.company,
parsed_args.year,
license_id,
company,
year,
)
if header:
fp.seek(0) # back to beginning of file
rest_of_file = fp.read()
fp.seek(0)
fp.write(header + "\n" + rest_of_file)
print(f"{file}: Added license header.")
return 0
return

except ValueError:
print(
f"{file}: No license header for the"
f" format {file.suffix} found.",
)
except FileNotFoundError:
print(
f"{file}: License file for {parsed_args.license_id} "
f"{file}: License file for {license_id} "
"is not existing."
)
return 1
return

# replace found header and write it to file
if copyright_match and (
not copyright_match["modification_year"]
and copyright_match["creation_year"] < parsed_args.year
or copyright_match["modification_year"]
and copyright_match["modification_year"] < parsed_args.year
not copyright_match.modification_year
and copyright_match.creation_year < year
or copyright_match.modification_year
and copyright_match.modification_year < year
):
copyright_term = (
f"SPDX-FileCopyrightText: "
f'{copyright_match["creation_year"]}'
f"-{parsed_args.year} {parsed_args.company}"
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
Expand All @@ -230,8 +223,8 @@ def _update_file(
fp.truncate()
print(
f"{file}: Changed License Header Copyright Year "
f'{copyright_match["modification_year"]} -> '
f"{parsed_args.year}"
f"{copyright_match.modification_year} -> "
f"{year}"
)

else:
Expand All @@ -251,12 +244,11 @@ def _update_file(
if new_content:
file.write_text(new_content, encoding="utf-8")
print(f"{file}: Cleaned up!")
return 0


def _get_exclude_list(
exclude_file: Path, directories: List[Path]
) -> List[Path]:
exclude_file: Path, directories: list[Path]
) -> list[Path]:
"""Tries to get the list of excluded files / directories.
If a file is given, it will be used. Otherwise it will be searched
in the executed root path.
Expand All @@ -266,12 +258,12 @@ def _get_exclude_list(

if exclude_file is None:
exclude_file = Path(".pontos-header-ignore")
try:
exclude_lines = exclude_file.read_text(encoding="utf-8").split("\n")
except FileNotFoundError:
print("No exclude list file found.")

if not exclude_file.is_file():
return []

exclude_lines = exclude_file.read_text(encoding="utf-8").splitlines()

expanded_globs = [
directory.rglob(line.strip())
for directory in directories
Expand All @@ -291,29 +283,32 @@ def _get_exclude_list(
return exclude_list


def _compile_outdated_regex() -> List[re.Pattern]:
@cache
def _compile_outdated_regex() -> list[re.Pattern]:
"""prepare regex patterns to remove old copyright lines"""
regexes: List[re.Pattern] = []
for line in OLD_LINES:
regexes.append(re.compile(rf"^(([#*]|//) ?)?{line}"))
return regexes
return [re.compile(rf"^(([#*]|//) ?)?{line}") for line in OLD_LINES]


def _compile_copyright_regex(company: Union[str, List[str]]) -> re.Pattern:
@cache
def _compile_copyright_regex() -> re.Pattern:
"""prepare the copyright regex"""
c_str = r"(SPDX-FileCopyrightText:|[Cc]opyright)"
d_str = r"(19[0-9]{2}|20[0-9]{2})"

if isinstance(company, str):
return re.compile(rf"{c_str}.*? {d_str}?-? ?{d_str}? ({company})")
return re.compile(rf"{c_str}.*? {d_str}?-? ?{d_str}? ({'|'.join(company)})")
return re.compile(rf"{c_str}.*? {d_str}?-? ?{d_str}? (.+)")


def main() -> None:
parsed_args = parse_args()
def main(args: Optional[Sequence[str]] = None) -> None:
parsed_args = parse_args(args)
exclude_list = []

if parsed_args.quiet:
year: str = parsed_args.year
license_id: str = parsed_args.license_id
company: str = parsed_args.company
changed: bool = parsed_args.changed
quiet: bool = parsed_args.quiet
cleanup: bool = parsed_args.cleanup

if quiet:
term: Union[NullTerminal, RichTerminal] = NullTerminal()
else:
term = RichTerminal()
Expand Down Expand Up @@ -347,25 +342,26 @@ def main() -> None:
term.error("Specify files to update!")
sys.exit(1)

copyright_regex: re.Pattern = _compile_copyright_regex(
company=[parsed_args.company, OLD_COMPANY]
)

cleanup_regexes: Optional[List[re.Pattern]] = None
if parsed_args.cleanup:
cleanup_regexes = _compile_outdated_regex()

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:
_update_file(
file=file,
copyright_regex=copyright_regex,
parsed_args=parsed_args,
term=term,
cleanup_regexes=cleanup_regexes,
update_file(
file,
year,
license_id,
company,
cleanup=cleanup,
)
except (FileNotFoundError, UnicodeDecodeError, ValueError):
continue
Expand Down
Loading
Loading