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

pantsbuild / pants / 19250292619

11 Nov 2025 12:09AM UTC coverage: 77.865% (-2.4%) from 80.298%
19250292619

push

github

web-flow
flag non-runnable targets used with `code_quality_tool` (#22875)

2 of 5 new or added lines in 2 files covered. (40.0%)

1487 existing lines in 72 files now uncovered.

71448 of 91759 relevant lines covered (77.86%)

3.22 hits per line

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

71.54
/src/python/pants/core/goals/package.py
1
# Copyright 2019 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
11✔
5

6
import logging
11✔
7
import os
11✔
8
from abc import ABCMeta
11✔
9
from collections.abc import Iterable
11✔
10
from dataclasses import dataclass
11✔
11
from string import Template
11✔
12

13
from pants.core.environments.rules import EnvironmentNameRequest, resolve_environment_name
11✔
14
from pants.core.util_rules import distdir
11✔
15
from pants.core.util_rules.distdir import DistDir
11✔
16
from pants.engine.addresses import Address
11✔
17
from pants.engine.environment import EnvironmentName
11✔
18
from pants.engine.fs import Digest, MergeDigests, Workspace
11✔
19
from pants.engine.goal import Goal, GoalSubsystem
11✔
20
from pants.engine.internals.graph import find_valid_field_sets
11✔
21
from pants.engine.internals.specs_rules import find_valid_field_sets_for_target_roots
11✔
22
from pants.engine.intrinsics import merge_digests
11✔
23
from pants.engine.rules import collect_rules, concurrently, goal_rule, implicitly, rule
11✔
24
from pants.engine.target import (
11✔
25
    AllTargets,
26
    AsyncFieldMixin,
27
    Dependencies,
28
    DepsTraversalBehavior,
29
    FieldSet,
30
    FieldSetsPerTargetRequest,
31
    NoApplicableTargetsBehavior,
32
    ShouldTraverseDepsPredicate,
33
    SpecialCasedDependencies,
34
    StringField,
35
    Target,
36
    TargetRootsToFieldSetsRequest,
37
    Targets,
38
)
39
from pants.engine.unions import UnionMembership, union
11✔
40
from pants.util.docutil import bin_name
11✔
41
from pants.util.logging import LogLevel
11✔
42
from pants.util.ordered_set import FrozenOrderedSet
11✔
43
from pants.util.strutil import help_text
11✔
44

45
logger = logging.getLogger(__name__)
11✔
46

47

48
@union(in_scope_types=[EnvironmentName])
11✔
49
class PackageFieldSet(FieldSet, metaclass=ABCMeta):
11✔
50
    """The fields necessary to build an asset from a target."""
51

52

53
@dataclass(frozen=True)
11✔
54
class BuiltPackageArtifact:
11✔
55
    """Information about artifacts in a built package.
56

57
    Used for logging information about the artifacts that are dumped to the distdir.
58
    """
59

60
    relpath: str | None
11✔
61
    extra_log_lines: tuple[str, ...] = tuple()
11✔
62

63

64
@dataclass(frozen=True)
11✔
65
class BuiltPackage:
11✔
66
    digest: Digest
11✔
67
    artifacts: tuple[BuiltPackageArtifact, ...]
11✔
68

69

70
@rule(polymorphic=True)
11✔
71
async def build_package(
11✔
72
    package_fieldset: PackageFieldSet,
73
    environment_name: EnvironmentName,
74
) -> BuiltPackage:
75
    raise NotImplementedError()
×
76

77

78
class OutputPathField(StringField, AsyncFieldMixin):
11✔
79
    DEFAULT = "${spec_path_normalized}/${target_name_normalized}${file_suffix}"
11✔
80

81
    alias = "output_path"
11✔
82
    default = DEFAULT
11✔
83

84
    help = help_text(
11✔
85
        f"""
86
        Where the built asset should be located.
87

88
        This field supports the following template replacements:
89

90
        - `${{spec_path_normalized}}`: The path to the target's directory ("spec path") with forward slashes replaced by dots.
91

92
        - `${{target_name_normalized}}`: The target's name with paramaterizations escaped by replacing dots with underscores.
93

94
        - `${{file_suffix}}`: For target's which produce single file artifacts, this is the file type suffix to use with a leading dot,
95
          and is empty otherwise when not applicable.
96

97
        If undefined, this will use the path to the BUILD file, followed by the target name.
98
        For example, `src/python/project:app` would be `src.python.project/app.ext`. This behavior corresponds to
99
        the default template: `{DEFAULT}`
100

101
        When running `{bin_name()} package`, this path will be prefixed by `--distdir` (e.g. `dist/`).
102

103
        Warning: setting this value risks naming collisions with other package targets you may have.
104
        """
105
    )
106

107
    def parameters(self, *, file_ending: str | None) -> dict[str, str]:
11✔
108
        spec_path_normalized = self.address.spec_path.replace(os.sep, ".")
5✔
109
        if not spec_path_normalized:
5✔
UNCOV
110
            spec_path_normalized = "."
×
111

112
        target_name_part = (
5✔
113
            self.address.generated_name.replace(".", "_")
114
            if self.address.generated_name
115
            else self.address.target_name
116
        )
117
        target_params_sanitized = self.address.parameters_repr.replace(".", "_")
5✔
118
        target_name_normalized = f"{target_name_part}{target_params_sanitized}"
5✔
119

120
        file_suffix = ""
5✔
121
        if file_ending:
5✔
122
            assert not file_ending.startswith("."), "`file_ending` should not start with `.`"
1✔
123
            file_suffix = f".{file_ending}"
1✔
124

125
        return dict(
5✔
126
            spec_path_normalized=spec_path_normalized,
127
            target_name_normalized=target_name_normalized,
128
            file_suffix=file_suffix,
129
        )
130

131
    def value_or_default(self, *, file_ending: str | None) -> str:
11✔
132
        template_string = self.value
5✔
133
        assert template_string is not None
5✔
134
        template = Template(template_string)
5✔
135

136
        params = self.parameters(file_ending=file_ending)
5✔
137
        result = template.safe_substitute(params)
5✔
138
        return os.path.normpath(result)
5✔
139

140

141
@dataclass(frozen=True)
11✔
142
class EnvironmentAwarePackageRequest:
11✔
143
    """Request class to request a `BuiltPackage` in an environment-aware fashion."""
144

145
    field_set: PackageFieldSet
11✔
146

147

148
@rule
11✔
149
async def environment_aware_package(request: EnvironmentAwarePackageRequest) -> BuiltPackage:
11✔
150
    environment_name = await resolve_environment_name(
×
151
        EnvironmentNameRequest.from_field_set(request.field_set), **implicitly()
152
    )
153
    package = await build_package(
×
154
        **implicitly({request.field_set: PackageFieldSet, environment_name: EnvironmentName})
155
    )
156
    return package
×
157

158

159
class PackageSubsystem(GoalSubsystem):
11✔
160
    name = "package"
11✔
161
    help = "Create a distributable package."
11✔
162

163
    @classmethod
11✔
164
    def activated(cls, union_membership: UnionMembership) -> bool:
11✔
165
        return PackageFieldSet in union_membership
×
166

167

168
class Package(Goal):
11✔
169
    subsystem_cls = PackageSubsystem
11✔
170
    environment_behavior = Goal.EnvironmentBehavior.USES_ENVIRONMENTS
11✔
171

172

173
class AllPackageableTargets(Targets):
11✔
174
    pass
11✔
175

176

177
@rule(desc="Find all packageable targets in project", level=LogLevel.DEBUG)
11✔
178
async def find_all_packageable_targets(all_targets: AllTargets) -> AllPackageableTargets:
11✔
179
    fs_per_target = await find_valid_field_sets(
×
180
        FieldSetsPerTargetRequest(PackageFieldSet, all_targets), **implicitly()
181
    )
182
    return AllPackageableTargets(
×
183
        target
184
        for target, field_sets in zip(all_targets, fs_per_target.collection)
185
        if len(field_sets) > 0
186
    )
187

188

189
@goal_rule
11✔
190
async def package_asset(workspace: Workspace, dist_dir: DistDir) -> Package:
11✔
191
    target_roots_to_field_sets = await find_valid_field_sets_for_target_roots(
×
192
        TargetRootsToFieldSetsRequest(
193
            PackageFieldSet,
194
            goal_description="the `package` goal",
195
            no_applicable_targets_behavior=NoApplicableTargetsBehavior.warn,
196
        ),
197
        **implicitly(),
198
    )
199
    if not target_roots_to_field_sets.field_sets:
×
200
        return Package(exit_code=0)
×
201

202
    packages = await concurrently(
×
203
        environment_aware_package(EnvironmentAwarePackageRequest(field_set))
204
        for field_set in target_roots_to_field_sets.field_sets
205
    )
206

207
    merged_digest = await merge_digests(MergeDigests(pkg.digest for pkg in packages))
×
208
    all_relpaths = [
×
209
        artifact.relpath for pkg in packages for artifact in pkg.artifacts if artifact.relpath
210
    ]
211

212
    workspace.write_digest(
×
213
        merged_digest, path_prefix=str(dist_dir.relpath), clear_paths=all_relpaths
214
    )
215
    for pkg in packages:
×
216
        for artifact in pkg.artifacts:
×
217
            msg = []
×
218
            if artifact.relpath:
×
219
                msg.append(f"Wrote {dist_dir.relpath / artifact.relpath}")
×
220
            msg.extend(str(line) for line in artifact.extra_log_lines)
×
221
            if msg:
×
222
                logger.info("\n".join(msg))
×
223
    return Package(exit_code=0)
×
224

225

226
@dataclass(frozen=True)
11✔
227
class TraverseIfNotPackageTarget(ShouldTraverseDepsPredicate):
11✔
228
    """This predicate stops dep traversal after package targets.
229

230
    When traversing deps, such as when collecting a list of transitive deps,
231
    this predicate effectively turns any package targets into graph leaf nodes.
232
    The package targets are included, but the deps of package targets are not.
233

234
    Also, this excludes dependencies from any SpecialCasedDependencies fields,
235
    which mirrors the behavior of the default predicate: TraverseIfDependenciesField.
236
    """
237

238
    package_field_set_types: FrozenOrderedSet[PackageFieldSet]
11✔
239
    roots: FrozenOrderedSet[Address]
11✔
240
    always_traverse_roots: bool  # traverse roots even if they are package targets
11✔
241

242
    def __init__(
11✔
243
        self,
244
        *,
245
        union_membership: UnionMembership,
246
        roots: Iterable[Address],
247
        always_traverse_roots: bool = True,
248
    ) -> None:
UNCOV
249
        object.__setattr__(self, "package_field_set_types", union_membership.get(PackageFieldSet))
×
UNCOV
250
        object.__setattr__(self, "roots", FrozenOrderedSet(roots))
×
UNCOV
251
        object.__setattr__(self, "always_traverse_roots", always_traverse_roots)
×
UNCOV
252
        super().__init__()
×
253

254
    def __call__(
11✔
255
        self, target: Target, field: Dependencies | SpecialCasedDependencies
256
    ) -> DepsTraversalBehavior:
257
        if isinstance(field, SpecialCasedDependencies):
×
258
            return DepsTraversalBehavior.EXCLUDE
×
259
        if self.always_traverse_roots and target.address in self.roots:
×
260
            return DepsTraversalBehavior.INCLUDE
×
261
        if any(
×
262
            field_set_type.is_applicable(target) for field_set_type in self.package_field_set_types
263
        ):
264
            return DepsTraversalBehavior.EXCLUDE
×
265
        return DepsTraversalBehavior.INCLUDE
×
266

267

268
def rules():
11✔
269
    return (*collect_rules(), *distdir.rules())
11✔
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