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

pantsbuild / pants / 20332790708

18 Dec 2025 09:48AM UTC coverage: 64.992% (-15.3%) from 80.295%
20332790708

Pull #22949

github

web-flow
Merge f730a56cd into 407284c67
Pull Request #22949: Add experimental uv resolver for Python lockfiles

54 of 97 new or added lines in 5 files covered. (55.67%)

8270 existing lines in 295 files now uncovered.

48990 of 75379 relevant lines covered (64.99%)

1.81 hits per line

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

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

4
from __future__ import annotations
4✔
5

6
from collections.abc import Mapping
4✔
7
from dataclasses import dataclass
4✔
8
from typing import ClassVar, TypeVar, Union
4✔
9

10
from pants.engine.addresses import Address
4✔
11
from pants.util.frozendict import FrozenDict
4✔
12
from pants.util.strutil import softwrap
4✔
13

14

15
class InterpolationError(ValueError):
4✔
16
    @classmethod
4✔
17
    def attribute_error(cls, value: str | InterpolationValue, attribute: str) -> InterpolationError:
4✔
UNCOV
18
        msg = f"The placeholder {attribute!r} is unknown."
×
UNCOV
19
        if value and isinstance(value, InterpolationValue):
×
UNCOV
20
            msg += f" Try with one of: {', '.join(value.keys())}."
×
UNCOV
21
        return cls(msg)
×
22

23

24
ErrorT = TypeVar("ErrorT", bound=InterpolationError)
4✔
25

26

27
class InterpolationValue(FrozenDict[str, str]):
4✔
28
    """Dict class suitable for use as a format string context object, as it allows to use attribute
29
    access rather than item access."""
30

31
    _attribute_error_type: ClassVar[type[InterpolationError]] = InterpolationError
4✔
32

33
    def __getattr__(self, attribute: str) -> str:
4✔
UNCOV
34
        if attribute not in self:
×
UNCOV
35
            raise self._attribute_error_type.attribute_error(self, attribute)
×
UNCOV
36
        return self[attribute]
×
37

38

39
class InterpolationContext(FrozenDict[str, Union[str, InterpolationValue]]):
4✔
40
    @classmethod
4✔
41
    def from_dict(cls, data: Mapping[str, str | Mapping[str, str]]) -> InterpolationContext:
4✔
UNCOV
42
        return InterpolationContext({key: cls.create_value(value) for key, value in data.items()})
×
43

44
    @staticmethod
4✔
45
    def create_value(value: str | Mapping[str, str]) -> str | InterpolationValue:
4✔
46
        """Ensure that `value` satisfies the type `InterpolationValue`."""
UNCOV
47
        if isinstance(value, (str, InterpolationValue)):
×
UNCOV
48
            return value
×
UNCOV
49
        return InterpolationValue(value)
×
50

51
    def merge(self, other: Mapping[str, str | Mapping[str, str]]) -> InterpolationContext:
4✔
52
        return InterpolationContext.from_dict({**self, **other})
×
53

54
    def format(
4✔
55
        self, text: str, *, source: TextSource, error_cls: type[ErrorT] | None = None
56
    ) -> str:
UNCOV
57
        stack = [text]
×
UNCOV
58
        try:
×
UNCOV
59
            while "{" in stack[-1] and "}" in stack[-1]:
×
UNCOV
60
                if len(stack) >= 5:
×
UNCOV
61
                    raise InterpolationError(
×
62
                        "The formatted placeholders recurse too deep.\n"
63
                        + " => ".join(map(repr, stack))
64
                    )
UNCOV
65
                stack.append(stack[-1].format(**self))
×
UNCOV
66
                if stack[-1] == stack[-2]:
×
67
                    break
×
UNCOV
68
            return stack[-1]
×
UNCOV
69
        except (KeyError, InterpolationError) as e:
×
UNCOV
70
            default_error_cls = InterpolationError
×
UNCOV
71
            msg = f"Invalid value for the {source}: {text!r}.\n\n"
×
UNCOV
72
            if isinstance(e, InterpolationError):
×
UNCOV
73
                default_error_cls = type(e)
×
UNCOV
74
                msg += str(e)
×
75
            else:
76
                # KeyError
UNCOV
77
                msg += f"The placeholder {e} is unknown."
×
UNCOV
78
                if self:
×
UNCOV
79
                    msg += f" Try with one of: {', '.join(sorted(self.keys()))}."
×
80
                else:
81
                    msg += " "
×
82
                    msg += softwrap(
×
83
                        f"""
84
                        There are currently no known placeholders to use.
85

86
                        Check the documentation of the {source} to understand where you may need
87
                        to configure your placeholders.
88
                        """
89
                    )
UNCOV
90
            raise (error_cls or default_error_cls)(msg) from e
×
91

92
    @dataclass(frozen=True)
4✔
93
    class TextSource:
4✔
94
        address: Address | None = None
4✔
95
        target_alias: str | None = None
4✔
96
        field_alias: str | None = None
4✔
97
        options_scope: str | None = None
4✔
98

99
        def __post_init__(self):
4✔
UNCOV
100
            field_infos_is_none = (
×
101
                x is None for x in [self.address, self.target_alias, self.field_alias]
102
            )
UNCOV
103
            if self.options_scope is None:
×
UNCOV
104
                assert not any(field_infos_is_none), f"Missing target field details in {self!r}."
×
105
            else:
UNCOV
106
                assert all(field_infos_is_none), (
×
107
                    f"Must not refer to both configuration option and target field in {self!r}."
108
                )
109

110
        def __str__(self) -> str:
4✔
UNCOV
111
            if self.options_scope:
×
112
                return f"`{self.options_scope}` configuration option"
×
UNCOV
113
            return (
×
114
                f"`{self.field_alias}` field of the `{self.target_alias}` target at {self.address}"
115
            )
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