Skip to content

Commit bb9d3f8

Browse files
scripts: add diff output to export_requirements
1 parent 550764d commit bb9d3f8

File tree

2 files changed

+111
-21
lines changed

2 files changed

+111
-21
lines changed

scripts/_utils.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Utility functions and variables which are useful for all scripts."""
2+
import difflib
3+
import importlib.util
4+
import os
5+
import pathlib
6+
import typing
7+
8+
9+
MODMAIL_DIR = pathlib.Path(importlib.util.find_spec("modmail").origin).parent
10+
PROJECT_DIR = MODMAIL_DIR.parent
11+
try:
12+
import pygments
13+
except ModuleNotFoundError:
14+
pygments = None
15+
else:
16+
from pygments.formatters import Terminal256Formatter
17+
from pygments.lexers.diff import DiffLexer
18+
19+
20+
class CheckFileEdit:
21+
"""Check if a file is edited within the body of this class."""
22+
23+
def __init__(self, *files: os.PathLike):
24+
self.files: typing.List[pathlib.Path] = []
25+
for f in files:
26+
self.files.append(pathlib.Path(f))
27+
self.return_value: typing.Optional[int] = None
28+
self.edited_files: typing.Dict[pathlib.Path] = dict()
29+
30+
def __enter__(self):
31+
self.file_contents = {}
32+
for file in self.files:
33+
try:
34+
with open(file, "r") as f:
35+
self.file_contents[file] = f.readlines()
36+
except FileNotFoundError:
37+
self.file_contents[file] = None
38+
return self
39+
40+
def __exit__(self, exc_type, exc_value, exc_traceback): # noqa: ANN001
41+
for file in self.files:
42+
with open(file, "r") as f:
43+
original_contents = self.file_contents[file]
44+
new_contents = f.readlines()
45+
if original_contents != new_contents:
46+
# construct a diff
47+
diff = difflib.unified_diff(
48+
original_contents, new_contents, fromfile="before", tofile="after"
49+
)
50+
try:
51+
diff = "".join(diff)
52+
except TypeError:
53+
diff = None
54+
else:
55+
if pygments is not None:
56+
diff = pygments.highlight(diff, DiffLexer(), Terminal256Formatter())
57+
self.edited_files[file] = diff
58+
59+
def write(self, path: str, contents: typing.Union[str, bytes], *, force: bool = False, **kwargs) -> bool:
60+
"""
61+
Write to the provided path with contents. Must be within the context manager.
62+
63+
Returns False if contents are not edited, True if they are.
64+
If force is True, will modify the files even if the contents match.
65+
66+
Any extras kwargs are passed to open()
67+
"""
68+
path = pathlib.Path(path)
69+
if path not in self.files:
70+
raise AssertionError(f"{path} must have been passed to __init__")
71+
72+
if not force:
73+
try:
74+
with open(path, "r") as f:
75+
if contents == f.read():
76+
return False
77+
except FileNotFoundError:
78+
pass
79+
if isinstance(contents, str):
80+
contents = contents.encode()
81+
82+
with open(path, "wb") as f:
83+
f.write(contents)
84+
85+
return True

scripts/export_requirements.py

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616

1717
import tomli
1818

19+
from ._utils import PROJECT_DIR, CheckFileEdit
1920

20-
GENERATED_FILE = pathlib.Path("requirements.txt")
21-
CONSTRAINTS_FILE = pathlib.Path("modmail/constraints.txt")
22-
DOC_REQUIREMENTS = pathlib.Path("docs/.requirements.txt")
21+
22+
GENERATED_FILE = PROJECT_DIR / "requirements.txt"
23+
CONSTRAINTS_FILE = PROJECT_DIR / "modmail/constraints.txt"
24+
DOC_REQUIREMENTS = PROJECT_DIR / "docs/.requirements.txt"
2325

2426
VERSION_RESTRICTER_REGEX = re.compile(r"(?P<sign>[<>=!]{1,2})(?P<version>\d+\.\d+?)(?P<patch>\.\d+?|\.\*)?")
2527
PLATFORM_MARKERS_REGEX = re.compile(r'sys_platform\s?==\s?"(?P<platform>\w+)"')
@@ -128,6 +130,7 @@ def _export_doc_requirements(toml: dict, file: pathlib.Path, *packages) -> int:
128130
file = pathlib.Path(file)
129131
if not file.exists():
130132
# file does not exist
133+
print(f"{file.relative_to(PROJECT_DIR)!s} must exist to export doc requirements")
131134
return 2
132135

133136
with open(file) as f:
@@ -149,14 +152,18 @@ def _export_doc_requirements(toml: dict, file: pathlib.Path, *packages) -> int:
149152
except AttributeError as e:
150153
print(e)
151154
return 3
152-
if new_contents == contents:
153-
# don't write anything, just return 0
154-
return 0
155155

156-
with open(file, "w") as f:
157-
f.write(new_contents)
156+
with CheckFileEdit(file) as check_file:
157+
158+
check_file.write(file, new_contents)
158159

159-
return 1
160+
for file, diff in check_file.edited_files.items():
161+
print(
162+
f"Exported new documentation requirements to {file.relative_to(PROJECT_DIR)!s}.",
163+
file=sys.stderr,
164+
)
165+
print(diff or "No diff to show.")
166+
print()
160167

161168

162169
def export(
@@ -269,19 +276,17 @@ def export(
269276
else:
270277
exit_code = 0
271278

272-
if req_path.exists():
273-
with open(req_path, "r") as f:
274-
if req_txt == f.read():
275-
# nothing to edit
276-
# if exit_code is ever removed from here, this should return zero
277-
return exit_code
279+
with CheckFileEdit(req_path) as check_file:
280+
check_file.write(req_path, req_txt)
278281

279-
if _write_file(req_path, req_txt):
280-
print(f"Updated {req_path} with new requirements.")
281-
return 1
282-
else:
283-
print(f"No changes were made to {req_path}")
284-
return 0
282+
for file, diff in check_file.edited_files.items():
283+
print(
284+
f"Exported new requirements to {file.relative_to(PROJECT_DIR)}.",
285+
file=sys.stderr,
286+
)
287+
print(diff or "No diff to show.")
288+
print()
289+
return bool(len(check_file.edited_files)) or exit_code
285290

286291

287292
def main(path: os.PathLike, include_markers: bool = True, **kwargs) -> int:

0 commit comments

Comments
 (0)