From 3d77c77d558fe978b038bba84223a07c7a4e98fa Mon Sep 17 00:00:00 2001 From: Gyanu Date: Thu, 27 Aug 2026 00:45:07 +0530 Subject: [PATCH] Treat a string path as a one-file group in newer_pairwise_group. Passing newer_group through newer_pairwise iterated a str as characters, so a real source file was almost always reported as missing. --- distutils/_modified.py | 22 ++++++++++++++++++++-- distutils/tests/test_modified.py | 18 ++++++++++++++++++ newsfragments/411.bugfix.rst | 1 + 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 newsfragments/411.bugfix.rst diff --git a/distutils/_modified.py b/distutils/_modified.py index 4033c0797..61468cd5c 100644 --- a/distutils/_modified.py +++ b/distutils/_modified.py @@ -2,7 +2,7 @@ from __future__ import annotations -import functools +import os import os.path from collections.abc import Callable, Iterable from typing import Literal, TypeVar @@ -91,4 +91,22 @@ def missing_as_newer(source): ) -newer_pairwise_group = functools.partial(newer_pairwise, newer=newer_group) +def _as_source_group(source): + if isinstance(source, (str, bytes, os.PathLike)): + return (source,) + return source + + +def newer_pairwise_group(sources, targets, missing: Literal["error", "ignore", "newer"] = "error"): + """ + Filter source groups that are newer than their paired targets. + + Each item in *sources* is a group of files passed to ``newer_group``. + A single path is treated as a one-file group, so a string is not + iterated as individual characters. + """ + + def _group_newer(source, target): + return newer_group(_as_source_group(source), target, missing=missing) + + return newer_pairwise(sources, targets, newer=_group_newer) diff --git a/distutils/tests/test_modified.py b/distutils/tests/test_modified.py index e35cec2d6..61c730f3a 100644 --- a/distutils/tests/test_modified.py +++ b/distutils/tests/test_modified.py @@ -119,6 +119,24 @@ def test_newer_pairwise_group(groups_target): assert newer == ([groups_target.newer], [groups_target.target]) +def test_newer_pairwise_group_single_path(tmp_path): + """A string path is a one-file group, not an iterable of characters.""" + src = tmp_path / "source.c" + old_tgt = tmp_path / "old.o" + new_tgt = tmp_path / "new.o" + src.write_text("", encoding="utf-8") + old_tgt.write_text("", encoding="utf-8") + new_tgt.write_text("", encoding="utf-8") + os.utime(src, (10, 10)) + os.utime(old_tgt, (1, 1)) + os.utime(new_tgt, (20, 20)) + + stale = newer_pairwise_group([str(src)], [str(new_tgt)]) + dirty = newer_pairwise_group([str(src)], [str(old_tgt)]) + assert stale == ([], []) + assert dirty == ([str(src)], [str(old_tgt)]) + + def test_newer_group_no_sources_no_target(tmp_path): """ Consider no sources and no target "newer". diff --git a/newsfragments/411.bugfix.rst b/newsfragments/411.bugfix.rst new file mode 100644 index 000000000..7bfdfa507 --- /dev/null +++ b/newsfragments/411.bugfix.rst @@ -0,0 +1 @@ +``newer_pairwise_group`` now treats a string path as a one-file group instead of iterating its characters. (pypa/distutils#411)