• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pantsbuild / pants / 19015773527

02 Nov 2025 05:33PM UTC coverage: 17.872% (-62.4%) from 80.3%
19015773527

Pull #22816

github

web-flow
Merge a12d75757 into 6c024e162
Pull Request #22816: Update Pants internal Python to 3.14

4 of 5 new or added lines in 3 files covered. (80.0%)

28452 existing lines in 683 files now uncovered.

9831 of 55007 relevant lines covered (17.87%)

0.18 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/python/pants/backend/docker/utils.py
1
# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

UNCOV
4
from __future__ import annotations
×
5

UNCOV
6
import difflib
×
UNCOV
7
import os.path
×
UNCOV
8
import re
×
UNCOV
9
from collections.abc import Callable, Iterable, Iterator, Sequence
×
UNCOV
10
from fnmatch import fnmatch
×
UNCOV
11
from typing import Self, TypeVar
×
12

UNCOV
13
from pants.help.maybe_color import MaybeColor
×
UNCOV
14
from pants.util.ordered_set import FrozenOrderedSet
×
15

UNCOV
16
_T = TypeVar("_T", bound="KeyValueSequenceUtil")
×
17

18

UNCOV
19
image_ref_regexp = re.compile(
×
20
    r"""
21
    ^
22
    # Optional registry.
23
    ((?P<registry>[^/:_ ]+:?[^/:_ ]*)/)?
24
    # Repository.
25
    (?P<repository>[^:@ \t\n\r\f\v]+)
26
    # Optionally with `:tag`.
27
    (:(?P<tag>[^@ ]+))?
28
    # Optionally with `@digest`.
29
    (@(?P<digest>\S+))?
30
    $
31
    """,
32
    re.VERBOSE,
33
)
34

35

UNCOV
36
class KeyValueSequenceUtil(FrozenOrderedSet[str]):
×
UNCOV
37
    @classmethod
×
UNCOV
38
    def from_strings(cls: type[_T], *strings: str, duplicates_must_match: bool = False) -> _T:
×
39
        """Takes all `KEY`/`KEY=VALUE` strings and dedupes by `KEY`.
40

41
        The last seen `KEY` wins in case of duplicates, unless `duplicates_must_match` is `True`, in
42
        which case all `VALUE`s must be equal, if present.
43
        """
44

UNCOV
45
        key_to_entry_and_value: dict[str, tuple[str, str | None]] = {}
×
UNCOV
46
        for entry in strings:
×
UNCOV
47
            key, has_value, value = entry.partition("=")
×
UNCOV
48
            if not duplicates_must_match:
×
49
                # Note that last entry with the same key wins.
UNCOV
50
                key_to_entry_and_value[key] = (entry, value if has_value else None)
×
51
            else:
52
                prev_entry, prev_value = key_to_entry_and_value.get(key, (None, None))
×
53
                if prev_entry is None:
×
54
                    # Not seen before.
55
                    key_to_entry_and_value[key] = (entry, value if has_value else None)
×
56
                elif not has_value:
×
57
                    # Seen before, no new value, so keep existing.
58
                    pass
×
59
                elif prev_value is None:
×
60
                    # Update value.
61
                    key_to_entry_and_value[key] = (entry, value)
×
62
                elif prev_value != value:
×
63
                    # Seen before with a different value.
64
                    raise ValueError(
×
65
                        f"{cls.__name__}: duplicated {key!r} with different values: "
66
                        f"{prev_value!r} != {value!r}."
67
                    )
68

UNCOV
69
        deduped_entries = sorted(
×
70
            entry_and_value[0] for entry_and_value in key_to_entry_and_value.values()
71
        )
UNCOV
72
        return cls(FrozenOrderedSet(deduped_entries))
×
73

UNCOV
74
    @classmethod
×
UNCOV
75
    def from_dict(cls, value: dict[str, str | None]) -> Self:
×
76
        def encode_kv(k: str, v: str | None) -> str:
×
77
            if v is None:
×
78
                return k
×
79
            return "=".join((k, v))
×
80

81
        return cls(FrozenOrderedSet(sorted(encode_kv(k, v) for k, v in value.items())))
×
82

UNCOV
83
    def to_dict(
×
84
        self, default: Callable[[str], str | None] = lambda x: None
85
    ) -> dict[str, str | None]:
UNCOV
86
        return {
×
87
            key: value if has_value else default(key)
88
            for key, has_value, value in [pair.partition("=") for pair in self]
89
        }
90

91

UNCOV
92
def suggest_renames(
×
93
    tentative_paths: Iterable[str], actual_files: Sequence[str], actual_dirs: Sequence[str]
94
) -> Iterator[tuple[str, str]]:
95
    """Return each pair of `tentative_paths` matched to the best possible match of `actual_paths`
96
    that are not an exact match.
97

98
    A pair of `(tentative_path, "")` means there were no possible match to find in the
99
    `actual_paths`, while a pair of `("", actual_path)` indicates a file in the build context that
100
    is not taking part in any `COPY` instruction.
101
    """
102

UNCOV
103
    actual_paths = (*actual_files, *actual_dirs)
×
UNCOV
104
    referenced: dict[str, set[str] | bool] = {}
×
105

UNCOV
106
    def reference(path: str) -> None:
×
107
        """Track which actual files has been referenced either explicitly by a tentative path, or as
108
        a suggested rename."""
UNCOV
109
        if path in actual_dirs:
×
UNCOV
110
            referenced[path] = True
×
111
        else:
UNCOV
112
            dirname = os.path.dirname(path)
×
UNCOV
113
            refs = referenced.setdefault(dirname, set())
×
UNCOV
114
            if isinstance(refs, set):
×
UNCOV
115
                refs.add(path)
×
116

117
        # Recalculate possible matches, to avoid suggesting the same file twice.
118
        nonlocal actual_paths
UNCOV
119
        actual_paths = tuple(get_unreferenced(actual_files, actual_dirs))
×
120

UNCOV
121
    def is_referenced(path: str, dirname: str | None = None) -> bool:
×
122
        """Check the list of referenced files to see if `path` has been flagged.
123

124
        Walks up the directory tree in case there is a recursive flag on one of the parent
125
        directories.
126
        """
UNCOV
127
        if dirname is None:
×
UNCOV
128
            dirname = os.path.dirname(path)
×
UNCOV
129
        refs = referenced.get(dirname, set())
×
UNCOV
130
        if isinstance(refs, bool):
×
UNCOV
131
            return refs
×
UNCOV
132
        if path in refs:
×
UNCOV
133
            return True
×
UNCOV
134
        parentdir = os.path.dirname(dirname)
×
UNCOV
135
        if parentdir:
×
UNCOV
136
            return is_referenced(path, parentdir)
×
UNCOV
137
        return False
×
138

UNCOV
139
    def get_unreferenced(files: Sequence[str] = (), dirs: Sequence[str] = ()) -> Iterator[str]:
×
UNCOV
140
        unreferenced_files = tuple(path for path in files if not is_referenced(path))
×
UNCOV
141
        yield from unreferenced_files
×
UNCOV
142
        for path in dirs:
×
UNCOV
143
            if not any(filename.startswith(path + "/") for filename in unreferenced_files):
×
144
                # Skip paths where we don't have any unreferenced files any longer.
UNCOV
145
                continue
×
UNCOV
146
            if not is_referenced(path, path):
×
UNCOV
147
                yield path
×
148

UNCOV
149
    def get_matches(path: str) -> tuple[str, ...]:
×
UNCOV
150
        is_pattern = any(all(c in path for c in cs) for cs in ["*", "?", "[]"])
×
UNCOV
151
        if not is_pattern:
×
UNCOV
152
            return (path,) if path in actual_paths else ()
×
153
        #
154
        # NOTICE: There is a slight difference in the pattern syntax used for the Dockerfile `COPY`
155
        # instruction, than what is implemented by the `fnmatch` function in Python.
156
        # https://docs.docker.com/engine/reference/builder/#copy which is implemented using
157
        # https://golang.org/pkg/path/filepath#Match compared to
158
        # https://docs.python.org/3/library/fnmatch.html#fnmatch.fnmatch
159
        #
160
        # For best experience when using globs, this should be addressed, but for now, I'll settle
161
        # for a "close enough" approximation to get this moving.
UNCOV
162
        return tuple(p for p in actual_paths if fnmatch(p, path))
×
163

164
    # Go over exact matches first, so we don't target them as possible matches for renames.
UNCOV
165
    unmatched_paths = set()
×
UNCOV
166
    for path in tentative_paths:
×
UNCOV
167
        matches = get_matches(path)
×
UNCOV
168
        for match in matches:
×
UNCOV
169
            reference(match)
×
UNCOV
170
        if not matches:
×
UNCOV
171
            unmatched_paths.add(path)
×
172

173
    # List unknown files, possibly with a rename suggestion.
UNCOV
174
    for path in sorted(unmatched_paths):
×
UNCOV
175
        for suggestion in difflib.get_close_matches(path, actual_paths, n=1, cutoff=0.1):
×
176
            # Suggest rename to match what files there are.
UNCOV
177
            reference(suggestion)
×
UNCOV
178
            yield path, suggestion
×
UNCOV
179
            break
×
180
        else:
181
            # No match for this path.
UNCOV
182
            yield path, ""
×
183

184
    # List unused files.
UNCOV
185
    for path in sorted(get_unreferenced(actual_files)):
×
UNCOV
186
        yield "", path
×
187

188

UNCOV
189
def format_rename_suggestion(src_path: str, dst_path: str, *, colors: bool) -> str:
×
190
    """Given two paths, formats a line showing what to change in `src_path` to get to `dst_path`."""
UNCOV
191
    color = MaybeColor(colors)
×
UNCOV
192
    rem = color.maybe_red(src_path)
×
UNCOV
193
    add = color.maybe_green(dst_path)
×
UNCOV
194
    return f"{rem} => {add}"
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc