Skip to content

Commit 99afb08

Browse files
committed
stdlib compat checking scripts
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
1 parent 2bf2332 commit 99afb08

File tree

3 files changed

+328
-0
lines changed

3 files changed

+328
-0
lines changed

scripts/checklist_template.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{% macro display_line(i) %}- {% if i.completed == True %}[x] {% elif i.completed == False %}[ ] {% endif %}{{ i.name }}{% if i.pr != None %} {{ i.pr }}{% endif %}{% endmacro %}
2+
# List of libraries
3+
4+
{% for lib in update_libs %}{{ display_line(lib) }}
5+
{% endfor %}
6+
7+
# List of un-added libraries
8+
These libraries are not added yet. Pure python one will be possible while others are not.
9+
10+
{% for lib in add_libs %}{{ display_line(lib) }}
11+
{% endfor %}
12+
13+
# List of tests without python libraries
14+
15+
{% for lib in update_tests %}{{ display_line(lib) }}
16+
{% endfor %}
17+
18+
# List of un-added tests without python libraries
19+
20+
{% for lib in add_tests %}{{ display_line(lib) }}
21+
{% endfor %}

scripts/find_eq.py

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Run differential queries to find equivalent files in cpython and rustpython
2+
# Arguments
3+
# --cpython: Path to cpython source code
4+
# --print-diff: Print the diff between the files
5+
# --color: Output color
6+
# --files: Optional globbing pattern to match files in cpython source code
7+
# --checklist: output as checklist
8+
9+
import argparse
10+
import difflib
11+
import pathlib
12+
13+
parser = argparse.ArgumentParser(description="Find equivalent files in cpython and rustpython")
14+
parser.add_argument("--cpython", type=pathlib.Path, required=True, help="Path to cpython source code")
15+
parser.add_argument("--print-diff", action="store_true", help="Print the diff between the files")
16+
parser.add_argument("--color", action="store_true", help="Output color")
17+
parser.add_argument("--files", type=str, default="*.py", help="Optional globbing pattern to match files in cpython source code")
18+
19+
args = parser.parse_args()
20+
21+
if not args.cpython.exists():
22+
raise FileNotFoundError(f"Path {args.cpython} does not exist")
23+
if not args.cpython.is_dir():
24+
raise NotADirectoryError(f"Path {args.cpython} is not a directory")
25+
if not args.cpython.is_absolute():
26+
args.cpython = args.cpython.resolve()
27+
28+
cpython_lib = args.cpython / "Lib"
29+
rustpython_lib = pathlib.Path(__file__).parent.parent / "Lib"
30+
assert rustpython_lib.exists(), "RustPython lib directory does not exist, ensure the find_eq.py script is located in the right place"
31+
32+
# walk through the cpython lib directory
33+
cpython_files = []
34+
for path in cpython_lib.rglob(args.files):
35+
if path.is_file():
36+
# remove the cpython lib path from the file path
37+
path = path.relative_to(cpython_lib)
38+
cpython_files.append(path)
39+
40+
for path in cpython_files:
41+
# check if the file exists in the rustpython lib directory
42+
rustpython_path = rustpython_lib / path
43+
if rustpython_path.exists():
44+
# open both files and compare them
45+
try:
46+
with open(cpython_lib / path, "r") as cpython_file:
47+
cpython_code = cpython_file.read()
48+
with open(rustpython_lib / path, "r") as rustpython_file:
49+
rustpython_code = rustpython_file.read()
50+
# compare the files
51+
diff = difflib.unified_diff(cpython_code.splitlines(), rustpython_code.splitlines(), lineterm="", fromfile=str(path), tofile=str(path))
52+
# print the diff if there are differences
53+
diff = list(diff)
54+
if len(diff) > 0:
55+
if args.print_diff:
56+
print("Differences:")
57+
for line in diff:
58+
print(line)
59+
else:
60+
print(f"File is not identical: {path}")
61+
else:
62+
print(f"File is identical: {path}")
63+
except Exception as e:
64+
print(f"Unable to check file {path}: {e}")
65+
else:
66+
print(f"File not found in RustPython: {path}")
67+
68+
# check for files in rustpython lib directory that are not in cpython lib directory
69+
rustpython_files = []
70+
for path in rustpython_lib.rglob(args.files):
71+
if path.is_file():
72+
# remove the rustpython lib path from the file path
73+
path = path.relative_to(rustpython_lib)
74+
rustpython_files.append(path)
75+
if path not in cpython_files:
76+
print(f"File not found in CPython: {path}")

scripts/generate_checklist.py

+231
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
# Arguments
2+
# --cpython: Path to cpython source code
3+
# --updated-libs: Libraries that have been updated in RustPython
4+
5+
6+
import argparse
7+
import dataclasses
8+
import difflib
9+
import pathlib
10+
from typing import Optional
11+
import warnings
12+
13+
import requests
14+
from jinja2 import Environment, FileSystemLoader
15+
16+
parser = argparse.ArgumentParser(description="Find equivalent files in cpython and rustpython")
17+
parser.add_argument("--cpython", type=pathlib.Path, required=True, help="Path to cpython source code")
18+
parser.add_argument("--updated-libs", type=pathlib.Path, required=False,
19+
help="Libraries that have been updated in RustPython")
20+
parser.add_argument("--notes", type=pathlib.Path, required=False, help="Path to notes file")
21+
22+
args = parser.parse_args()
23+
24+
def check_pr(pr_id) -> bool:
25+
if pr_id.startswith("#"):
26+
pr_id = pr_id[1:]
27+
pr_id = int(pr_id)
28+
req = f"https://api.github.com/repos/RustPython/RustPython/pulls/{pr_id}"
29+
response = requests.get(req).json()
30+
return response["merged_at"] is not None
31+
32+
@dataclasses.dataclass
33+
class LibUpdate:
34+
pr: Optional[str] = None
35+
done: bool = True
36+
37+
updated_libs = {}
38+
if args.updated_libs:
39+
# check if the file exists in the rustpython lib directory
40+
updated_libs_path = args.updated_libs
41+
if updated_libs_path.exists():
42+
with open(updated_libs_path) as f:
43+
for line in f:
44+
line = line.strip()
45+
if not line.startswith("//") and line:
46+
line = line.split(" ")
47+
if len(line) == 2:
48+
is_done = True
49+
try:
50+
is_done = check_pr(line[1])
51+
except Exception as e:
52+
warnings.warn(f"Failed to check PR {line[1]}: {e}")
53+
updated_libs[line[0]] = LibUpdate(line[1])
54+
elif len(line) == 1:
55+
updated_libs[line[0]] = LibUpdate()
56+
else:
57+
raise ValueError(f"Invalid line: {line}")
58+
59+
else:
60+
raise FileNotFoundError(f"Path {updated_libs_path} does not exist")
61+
if not args.cpython.exists():
62+
raise FileNotFoundError(f"Path {args.cpython} does not exist")
63+
if not args.cpython.is_dir():
64+
raise NotADirectoryError(f"Path {args.cpython} is not a directory")
65+
if not args.cpython.is_absolute():
66+
args.cpython = args.cpython.resolve()
67+
68+
notes = {}
69+
if args.notes:
70+
# check if the file exists in the rustpython lib directory
71+
notes_path = args.notes
72+
if notes_path.exists():
73+
with open(notes_path) as f:
74+
for line in f:
75+
line = line.strip()
76+
if not line.startswith("//") and line:
77+
line = line.split(" ")
78+
if len(line) > 1:
79+
rest = " ".join(line[1:])
80+
if line[0] in notes:
81+
notes[line[0]].append(rest)
82+
else:
83+
notes[line[0]] = [rest]
84+
else:
85+
raise ValueError(f"Invalid note: {line}")
86+
87+
else:
88+
raise FileNotFoundError(f"Path {notes_path} does not exist")
89+
90+
cpython_lib = args.cpython / "Lib"
91+
rustpython_lib = pathlib.Path(__file__).parent.parent / "Lib"
92+
assert rustpython_lib.exists(), "RustPython lib directory does not exist, ensure the find_eq.py script is located in the right place"
93+
94+
ignored_objs = [
95+
"__pycache__",
96+
"test"
97+
]
98+
# loop through the top-level directories in the cpython lib directory
99+
libs = []
100+
for path in cpython_lib.iterdir():
101+
if path.is_dir() and path.name not in ignored_objs:
102+
# add the directory name to the list of libraries
103+
libs.append(path.name)
104+
elif path.is_file() and path.name.endswith(".py") and path.name not in ignored_objs:
105+
# add the file name to the list of libraries
106+
libs.append(path.name)
107+
108+
tests = []
109+
cpython_lib_test = cpython_lib / "test"
110+
for path in cpython_lib_test.iterdir():
111+
if path.is_dir() and path.name not in ignored_objs and path.name.startswith("test_"):
112+
# add the directory name to the list of libraries
113+
tests.append(path.name)
114+
elif path.is_file() and path.name.endswith(".py") and path.name not in ignored_objs and path.name.startswith("test_"):
115+
# add the file name to the list of libraries
116+
file_name = path.name.replace("test_", "")
117+
if file_name not in libs and file_name.replace(".py", "") not in libs:
118+
tests.append(path.name)
119+
120+
def check_diff(file1, file2):
121+
try:
122+
with open(file1, "r") as f1, open(file2, "r") as f2:
123+
f1_lines = f1.readlines()
124+
f2_lines = f2.readlines()
125+
diff = difflib.unified_diff(f1_lines, f2_lines, lineterm="")
126+
diff_lines = list(diff)
127+
return len(diff_lines)
128+
except UnicodeDecodeError:
129+
return False
130+
131+
def check_completion_pr(display_name):
132+
for lib in updated_libs:
133+
if lib == str(display_name):
134+
return updated_libs[lib].done, updated_libs[lib].pr
135+
return False, None
136+
137+
def check_test_completion(rustpython_path, cpython_path):
138+
if rustpython_path.exists() and rustpython_path.is_file():
139+
if cpython_path.exists() and cpython_path.is_file():
140+
if not rustpython_path.exists() or not rustpython_path.is_file():
141+
return False
142+
elif check_diff(rustpython_path, cpython_path) > 0:
143+
return False
144+
return True
145+
return False
146+
147+
def check_lib_completion(rustpython_path, cpython_path):
148+
test_name = "test_" + rustpython_path.name
149+
rustpython_test_path = rustpython_lib / "test" / test_name
150+
cpython_test_path = cpython_lib / "test" / test_name
151+
if cpython_test_path.exists() and not check_test_completion(rustpython_test_path, cpython_test_path):
152+
return False
153+
if rustpython_path.exists() and rustpython_path.is_file():
154+
if check_diff(rustpython_path, cpython_path) > 0:
155+
return False
156+
return True
157+
return False
158+
159+
def handle_notes(display_path) -> list[str]:
160+
if str(display_path) in notes:
161+
res = notes[str(display_path)]
162+
# remove the note from the notes list
163+
del notes[str(display_path)]
164+
return res
165+
return []
166+
167+
@dataclasses.dataclass
168+
class Output:
169+
name: str
170+
pr: Optional[str]
171+
completed: Optional[bool]
172+
notes: list[str]
173+
174+
update_libs_output = []
175+
add_libs_output = []
176+
for path in libs:
177+
# check if the file exists in the rustpython lib directory
178+
rustpython_path = rustpython_lib / path
179+
# remove the file extension if it exists
180+
display_path = pathlib.Path(path).with_suffix("")
181+
(completed, pr) = check_completion_pr(display_path)
182+
if rustpython_path.exists():
183+
if not completed:
184+
# check if the file exists in the cpython lib directory
185+
cpython_path = cpython_lib / path
186+
# check if the file exists in the rustpython lib directory
187+
if rustpython_path.exists() and rustpython_path.is_file():
188+
completed = check_lib_completion(rustpython_path, cpython_path)
189+
update_libs_output.append(Output(str(display_path), pr, completed, handle_notes(display_path)))
190+
else:
191+
if pr is not None and completed:
192+
update_libs_output.append(Output(str(display_path), pr, None, handle_notes(display_path)))
193+
else:
194+
add_libs_output.append(Output(str(display_path), pr, None, handle_notes(display_path)))
195+
196+
update_tests_output = []
197+
add_tests_output = []
198+
for path in tests:
199+
# check if the file exists in the rustpython lib directory
200+
rustpython_path = rustpython_lib / "test" / path
201+
# remove the file extension if it exists
202+
display_path = pathlib.Path(path).with_suffix("")
203+
(completed, pr) = check_completion_pr(display_path)
204+
if rustpython_path.exists():
205+
if not completed:
206+
# check if the file exists in the cpython lib directory
207+
cpython_path = cpython_lib / "test" / path
208+
# check if the file exists in the rustpython lib directory
209+
if rustpython_path.exists() and rustpython_path.is_file():
210+
completed = check_lib_completion(rustpython_path, cpython_path)
211+
update_tests_output.append(Output(str(display_path), pr, completed, handle_notes(display_path)))
212+
else:
213+
if pr is not None and completed:
214+
update_tests_output.append(Output(str(display_path), pr, None, handle_notes(display_path)))
215+
else:
216+
add_tests_output.append(Output(str(display_path), pr, None, handle_notes(display_path)))
217+
218+
for note in notes:
219+
# add a warning for each note that is not attached to a file
220+
for n in notes[note]:
221+
warnings.warn(f"Unattached Note: {note} - {n}")
222+
223+
env = Environment(loader=FileSystemLoader('.'))
224+
template = env.get_template("checklist_template.md")
225+
output = template.render(
226+
update_libs=update_libs_output,
227+
add_libs=add_libs_output,
228+
update_tests=update_tests_output,
229+
add_tests=add_tests_output
230+
)
231+
print(output)

0 commit comments

Comments
 (0)