gh-64595: Touch the source file if the generated file was changed (GH-155265)
serhiy-storchaka <[email protected]>
| Newsgroups | gmane.comp.python.cvs |
|---|---|
| Message-ID | <[email protected]> |
https://github.com/python/cpython/commit/c5168eadf15665310b22ff44e23738034d3e5036 commit: c5168eadf15665310b22ff44e23738034d3e5036 branch: main author: Serhiy Storchaka <[email protected]> committer: serhiy-storchaka <[email protected]> date: 2026-08-18T19:55:02+03:00 summary: gh-64595: Touch the source file if the generated file was changed (GH-155265) The build system does not know that the source file depends on the files generated from it, so it did not recompile the source file if only the generated file was changed. The generated files are also kept newer than the source file. files: A Misc/NEWS.d/next/Tools-Demos/2026-08-06-09-43-16.gh-issue-64595.W75XZK.rst M Lib/test/test_clinic.py M Tools/clinic/libclinic/cli.py M Tools/clinic/libclinic/utils.py diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index f0dc62967f6a776..94a69b6d7309df8 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -3214,6 +3214,64 @@ def test_no_change(self): # if the content does not change self.assertEqual(pre_mtime, post_mtime) + TOUCH_CODE = dedent(""" + /*[clinic input] + module m + [clinic start generated code]*/ + + /*[clinic input] + output everything file + m.func + a: int + / + + Docstring. + [clinic start generated code]*/ + """) + + def test_touch_source(self): + # gh-64595: The build system does not know that the source file + # depends on the file generated from it, so the modification + # times are updated to force the recompilation. + def mtimes(): + return os.stat(fn).st_mtime_ns, os.stat(dest).st_mtime_ns + + def set_mtimes(source, generated): + os.utime(fn, ns=(source, source)) + os.utime(dest, ns=(generated, generated)) + + with os_helper.temp_dir() as tmp_dir: + fn = os.path.join(tmp_dir, "test.c") + with open(fn, "w", encoding="utf-8") as f: + f.write(self.TOUCH_CODE) + dest = self.dest_file(fn) + self.expect_success(fn) + source_mtime, generated_mtime = mtimes() + self.assertGreaterEqual(generated_mtime, source_mtime) + + # The generated file is changed, so both files are touched. + os.unlink(dest) + old = source_mtime - 10**10 + os.utime(fn, ns=(old, old)) + self.expect_success(fn) + source_mtime, generated_mtime = mtimes() + self.assertGreater(source_mtime, old) + self.assertGreaterEqual(generated_mtime, source_mtime) + + # Nothing is changed, but the source file is newer, so only + # the generated file is touched. + set_mtimes(source_mtime - 10**10, source_mtime - 2 * 10**10) + old_source_mtime = os.stat(fn).st_mtime_ns + self.expect_success(fn) + source_mtime, generated_mtime = mtimes() + self.assertEqual(source_mtime, old_source_mtime) + self.assertGreaterEqual(generated_mtime, source_mtime) + + # Nothing is changed and the generated file is newer, + # so no file is touched. + self.expect_success(fn) + self.assertEqual(mtimes(), (source_mtime, generated_mtime)) + def test_cli_force(self): invalid_input = dedent(""" /*[clinic input] diff --git a/Misc/NEWS.d/next/Tools-Demos/2026-08-06-09-43-16.gh-issue-64595.W75XZK.rst b/Misc/NEWS.d/next/Tools-Demos/2026-08-06-09-43-16.gh-issue-64595.W75XZK.rst new file mode 100644 index 000000000000000..6c9393347d31ce5 --- /dev/null +++ b/Misc/NEWS.d/next/Tools-Demos/2026-08-06-09-43-16.gh-issue-64595.W75XZK.rst @@ -0,0 +1,5 @@ +Argument Clinic now touches the source file if a file generated from it was +changed, and keeps the generated files newer than the source file. +The build system does not know that the source file depends on the files +generated from it, so it did not recompile the source file if only the +generated file was changed. diff --git a/Tools/clinic/libclinic/cli.py b/Tools/clinic/libclinic/cli.py index c66084cf3144826..290fc3a6e59408a 100644 --- a/Tools/clinic/libclinic/cli.py +++ b/Tools/clinic/libclinic/cli.py @@ -89,10 +89,15 @@ def parse_file( filename=filename, limited_capi=limited_capi, writer=writer) + index = len(writer.files) cooked = clinic.parse(raw) - writer.write(output, cooked) + files = writer.files[index:] + writer.update_times(output, + [fn for fn, _ in files if fn != output], + any(changed for _, changed in files)) + def create_cli() -> argparse.ArgumentParser: cmdline = argparse.ArgumentParser( diff --git a/Tools/clinic/libclinic/utils.py b/Tools/clinic/libclinic/utils.py index 8fc8748f0f9ae10..01015ff1237656b 100644 --- a/Tools/clinic/libclinic/utils.py +++ b/Tools/clinic/libclinic/utils.py @@ -5,6 +5,7 @@ import os import re import string +from collections.abc import Iterable from typing import Literal, Final @@ -17,11 +18,14 @@ def read_file(filename: str) -> str | None: return None -def write_file(filename: str, new_contents: str) -> None: - """Write new content to file, iff the content changed.""" +def write_file(filename: str, new_contents: str) -> bool: + """Write new content to file, iff the content changed. + + Return True if the file was written. + """ if read_file(filename) == new_contents: # no change: avoid modifying the file modification time - return + return False # Atomic write using a temporary file and os.replace() filename_new = f"{filename}.new" with open(filename_new, "w", encoding="utf-8") as fp: @@ -31,6 +35,7 @@ def write_file(filename: str, new_contents: str) -> None: except: os.unlink(filename_new) raise + return True @dc.dataclass(slots=True, frozen=True) @@ -50,6 +55,8 @@ class FileWriter: dry_run: bool = False changes: list[FileChange] = dc.field(default_factory=list) + # (filename, changed) for every file which was passed to write(). + files: list[tuple[str, bool]] = dc.field(default_factory=list) def makedirs(self, dirname: str) -> None: if not self.dry_run: @@ -61,12 +68,34 @@ def makedirs(self, dirname: str) -> None: def write(self, filename: str, new_contents: str) -> None: if not self.dry_run: - write_file(filename, new_contents) + changed = write_file(filename, new_contents) + else: + old_contents = read_file(filename) + changed = old_contents != new_contents + if changed: + self.changes.append( + FileChange(filename, old_contents, new_contents)) + self.files.append((filename, changed)) + + def update_times(self, source: str, generated: Iterable[str], + changed: bool) -> None: + """Keep the generated files newer than the source file. + + The build system does not know that the source file depends on + the files generated from it, so the source file is touched to + force its recompilation. + """ + if self.dry_run: return - old_contents = read_file(filename) - if old_contents != new_contents: - self.changes.append( - FileChange(filename, old_contents, new_contents)) + if changed: + os.utime(source) + for filename in generated: + os.utime(filename) + else: + mtime = os.stat(source).st_mtime_ns + for filename in generated: + if os.stat(filename).st_mtime_ns <= mtime: + os.utime(filename) def compute_checksum(input_: str, length: int | None = None) -> str: _______________________________________________ Python-checkins mailing list -- [email protected] To unsubscribe send an email to [email protected] https://mail.python.org/mailman3//lists/python-checkins.python.org Member address: [email protected]