Skip to content

Commit af7d480

Browse files
committed
include the program used for creating copyright headers
1 parent 9416fb2 commit af7d480

File tree

1 file changed

+94
-0
lines changed

1 file changed

+94
-0
lines changed

devtools/update_copyright.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Copyright (c) 2025.
2+
# Authors: Tamás K. Stenczel
3+
# This program is distributed under the MIT License, see LICENSE.md.
4+
5+
"""Add or update copyright notice to all python files."""
6+
7+
from datetime import date
8+
import os
9+
import re
10+
import subprocess
11+
12+
# git users -> real name mapping for current contributors
13+
USER_TO_REAL_NAME = {
14+
"stenczelt": "Tamás K. Stenczel",
15+
"Tamas K Stenczel": "Tamás K. Stenczel",
16+
"Tamas Stenczel": "Tamás K. Stenczel",
17+
"T. K. Stenczel": "Tamás K. Stenczel",
18+
"Adam Fekete": "Ádám Fekete",
19+
"ElliottKasoar": "Elliott Kasoar",
20+
"gelzinyte": "Elena Gelzinyte",
21+
"gabor1": "Gábor Csányi",
22+
"gabor": "Gábor Csányi",
23+
}
24+
25+
26+
def get_contributors(file_path):
27+
"""Get list of contributors for a given file."""
28+
command = f"git log --pretty=format:'%an' {file_path} | sort | uniq"
29+
contributors = subprocess.check_output(command, shell=True, text=True)
30+
contributors = contributors.strip().split("\n")
31+
32+
# Map usernames to real names, if possible
33+
real_name_contributors = []
34+
for contributor in contributors:
35+
if contributor == "":
36+
continue
37+
real_name = USER_TO_REAL_NAME[contributor]
38+
if real_name not in real_name_contributors:
39+
real_name_contributors.append(real_name)
40+
41+
return real_name_contributors
42+
43+
44+
def insert_copyright(file_path, contributors):
45+
"""Insert a copyright notice at the top of a file."""
46+
# Create the copyright notice string
47+
author_names = ", ".join(contributors)
48+
copyright_notice = (
49+
f"# Copyright (c) {date.today().year}.\n"
50+
f"# Authors: {author_names}\n"
51+
f"# This program is distributed under the MIT License, see LICENSE.md.\n\n"
52+
)
53+
54+
with open(file_path) as file:
55+
content = file.read()
56+
57+
# replace existing notice if needed
58+
pattern = re.compile(
59+
r"^# Copyright \(c\) [\d-]+\.\n"
60+
r"# Authors:\s*(.*?)\n"
61+
r"# This program is distributed under the MIT License, see LICENSE\.md\.\n\n",
62+
re.MULTILINE,
63+
)
64+
65+
if m := pattern.match(content):
66+
if m.group(1) == author_names:
67+
print(file_path, "Matched! contributors unchanged")
68+
else:
69+
print(file_path, "Updating contributors: ", m.group(1), " ->", author_names)
70+
content = pattern.sub(copyright_notice, content)
71+
else:
72+
print(file_path, "adding contributors", author_names)
73+
content = copyright_notice + content
74+
75+
# Insert the copyright notice at the top
76+
with open(file_path, "w") as file:
77+
file.write(content)
78+
79+
80+
def process_files(directory):
81+
"""Process all Python files in the given directory."""
82+
for root, _, files in os.walk(directory):
83+
if root.startswith("./.venv"):
84+
continue
85+
for file in files:
86+
if file.endswith(".py") and file != "__init__.py":
87+
file_path = os.path.join(root, file)
88+
contributors = get_contributors(file_path)
89+
insert_copyright(file_path, contributors)
90+
91+
92+
if __name__ == "__main__":
93+
# Replace with your folder path
94+
process_files(".")

0 commit comments

Comments
 (0)