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

pantsbuild / pants / 22285099215

22 Feb 2026 08:52PM UTC coverage: 75.854% (-17.1%) from 92.936%
22285099215

Pull #23121

github

web-flow
Merge c7299df9c into ba8359840
Pull Request #23121: fix issue with optional fields in dependency validator

28 of 29 new or added lines in 2 files covered. (96.55%)

11174 existing lines in 400 files now uncovered.

53694 of 70786 relevant lines covered (75.85%)

1.88 hits per line

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

46.3
/src/python/pants/backend/python/macros/common_requirements_rule.py
1
# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
2✔
5

6
import itertools
2✔
7
import logging
2✔
8
import os
2✔
9
from collections.abc import Callable, Iterable
2✔
10
from typing import cast
2✔
11

12
from packaging.utils import canonicalize_name as canonicalize_project_name
2✔
13

14
from pants.backend.python.goals.lockfile import synthetic_lockfile_target_name
2✔
15
from pants.backend.python.macros.common_fields import (
2✔
16
    ModuleMappingField,
17
    TypeStubsModuleMappingField,
18
)
19
from pants.backend.python.subsystems.setup import PythonSetup
2✔
20
from pants.backend.python.target_types import (
2✔
21
    PythonRequirementModulesField,
22
    PythonRequirementResolveField,
23
    PythonRequirementsField,
24
    PythonRequirementTarget,
25
    PythonRequirementTypeStubModulesField,
26
)
27
from pants.core.target_types import (
2✔
28
    TargetGeneratorSourcesHelperSourcesField,
29
    TargetGeneratorSourcesHelperTarget,
30
)
31
from pants.engine.addresses import Address
2✔
32
from pants.engine.fs import GlobMatchErrorBehavior, PathGlobs
2✔
33
from pants.engine.internals.build_files import find_target_adaptor
2✔
34
from pants.engine.internals.target_adaptor import TargetAdaptorRequest
2✔
35
from pants.engine.intrinsics import get_digest_contents
2✔
36
from pants.engine.rules import implicitly
2✔
37
from pants.engine.target import (
2✔
38
    Dependencies,
39
    GenerateTargetsRequest,
40
    InvalidFieldException,
41
    SingleSourceField,
42
)
43
from pants.engine.unions import UnionMembership
2✔
44
from pants.util.pip_requirement import PipRequirement
2✔
45
from pants.util.strutil import softwrap
2✔
46

47
logger = logging.getLogger(__name__)
2✔
48
ParseRequirementsCallback = Callable[[bytes, str], Iterable[PipRequirement]]
2✔
49

50

51
async def _generate_requirements(
2✔
52
    request: GenerateTargetsRequest,
53
    union_membership: UnionMembership,
54
    python_setup: PythonSetup,
55
    parse_requirements_callback: ParseRequirementsCallback,
56
) -> Iterable[PythonRequirementTarget]:
UNCOV
57
    generator = request.generator
×
UNCOV
58
    requirements_rel_path = generator[SingleSourceField].value
×
UNCOV
59
    requirements_full_path = generator[SingleSourceField].file_path
×
UNCOV
60
    overrides = {
×
61
        canonicalize_project_name(k): v
62
        for k, v in request.require_unparametrized_overrides().items()
63
    }
64

65
    # Pretend this is just another generated target, for typing purposes.
UNCOV
66
    file_tgt = cast(
×
67
        "PythonRequirementTarget",
68
        TargetGeneratorSourcesHelperTarget(
69
            {TargetGeneratorSourcesHelperSourcesField.alias: requirements_rel_path},
70
            Address(
71
                request.template_address.spec_path,
72
                target_name=request.template_address.target_name,
73
                relative_file_path=requirements_rel_path,
74
            ),
75
            union_membership,
76
        ),
77
    )
78

UNCOV
79
    req_deps = [file_tgt.address.spec]
×
80

UNCOV
81
    resolve = request.template.get(
×
82
        PythonRequirementResolveField.alias, python_setup.default_resolve
83
    )
UNCOV
84
    lockfile = (
×
85
        python_setup.resolves.get(resolve) if python_setup.enable_synthetic_lockfiles else None
86
    )
UNCOV
87
    if lockfile:
×
UNCOV
88
        lockfile_address = Address(
×
89
            os.path.dirname(lockfile),
90
            target_name=synthetic_lockfile_target_name(resolve),
91
        )
UNCOV
92
        target_adaptor = await find_target_adaptor(
×
93
            TargetAdaptorRequest(
94
                description_of_origin=f"{generator.alias} lockfile dep for the {resolve} resolve",
95
                address=lockfile_address,
96
            ),
97
        )
UNCOV
98
        if target_adaptor.type_alias == "_lockfiles":
×
UNCOV
99
            req_deps.append(f"{lockfile}:{synthetic_lockfile_target_name(resolve)}")
×
100
        else:
UNCOV
101
            logger.warning(
×
102
                softwrap(
103
                    f"""
104
                    The synthetic lockfile target for {lockfile} is being shadowed by the
105
                    {target_adaptor.type_alias} target {lockfile_address}.
106

107
                    There will not be any dependency to the lockfile.
108

109
                    Resolve by either renaming the shadowing target, the resolve {resolve!r} or
110
                    moving the target or the lockfile to another directory.
111
                    """
112
                )
113
            )
114

UNCOV
115
    digest_contents = await get_digest_contents(
×
116
        **implicitly(
117
            PathGlobs(
118
                [requirements_full_path],
119
                glob_match_error_behavior=GlobMatchErrorBehavior.error,
120
                description_of_origin=f"{generator}'s field `{SingleSourceField.alias}`",
121
            )
122
        )
123
    )
124

UNCOV
125
    module_mapping = generator[ModuleMappingField].value
×
UNCOV
126
    stubs_mapping = generator[TypeStubsModuleMappingField].value
×
127

UNCOV
128
    def generate_tgt(
×
129
        project_name: str, parsed_reqs: Iterable[PipRequirement]
130
    ) -> PythonRequirementTarget:
UNCOV
131
        normalized_proj_name = canonicalize_project_name(project_name)
×
UNCOV
132
        tgt_overrides = overrides.pop(normalized_proj_name, {})
×
UNCOV
133
        if Dependencies.alias in tgt_overrides:
×
UNCOV
134
            tgt_overrides = tgt_overrides | {  # type: ignore[operator]
×
135
                Dependencies.alias: list(tgt_overrides[Dependencies.alias]) + req_deps
136
            }
137

UNCOV
138
        return PythonRequirementTarget(
×
139
            {
140
                **request.template,
141
                PythonRequirementsField.alias: list(parsed_reqs),
142
                PythonRequirementModulesField.alias: module_mapping.get(normalized_proj_name),
143
                PythonRequirementTypeStubModulesField.alias: stubs_mapping.get(
144
                    normalized_proj_name
145
                ),
146
                # This may get overridden by `tgt_overrides`, which will have already added in
147
                # the file tgt.
148
                Dependencies.alias: req_deps,
149
                **tgt_overrides,
150
            },
151
            request.template_address.create_generated(project_name),
152
            union_membership,
153
        )
154

UNCOV
155
    requirements = parse_requirements_callback(digest_contents[0].content, requirements_full_path)
×
UNCOV
156
    grouped_requirements = itertools.groupby(requirements, lambda parsed_req: parsed_req.name)
×
UNCOV
157
    result = tuple(
×
158
        generate_tgt(project_name, parsed_reqs_)
159
        for project_name, parsed_reqs_ in grouped_requirements
160
    ) + (file_tgt,)
161

UNCOV
162
    if overrides:
×
163
        raise InvalidFieldException(
×
164
            softwrap(
165
                f"""
166
                Unused key in the `overrides` field for {request.template_address}:
167
                {sorted(overrides)}
168
                """
169
            )
170
        )
171

UNCOV
172
    return result
×
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

© 2026 Coveralls, Inc