• 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

79.19
/src/python/pants/jvm/target_types.py
1
# Copyright 2020 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 dataclasses
11✔
7
import re
11✔
8
import xml.etree.ElementTree as ET
11✔
9
from abc import ABC, ABCMeta, abstractmethod
11✔
10
from collections.abc import Callable, Iterable, Iterator
11✔
11
from dataclasses import dataclass
11✔
12
from typing import ClassVar
11✔
13

14
from pants.build_graph.build_file_aliases import BuildFileAliases
11✔
15
from pants.core.goals.generate_lockfiles import UnrecognizedResolveNamesError
11✔
16
from pants.core.goals.package import OutputPathField
11✔
17
from pants.core.goals.run import RestartableField, RunFieldSet, RunInSandboxBehavior
11✔
18
from pants.core.goals.test import TestExtraEnvVarsField, TestTimeoutField
11✔
19
from pants.core.util_rules.source_files import SourceFilesRequest, determine_source_files
11✔
20
from pants.engine.addresses import Address
11✔
21
from pants.engine.intrinsics import get_digest_contents
11✔
22
from pants.engine.rules import collect_rules, rule
11✔
23
from pants.engine.target import (
11✔
24
    COMMON_TARGET_FIELDS,
25
    AsyncFieldMixin,
26
    BoolField,
27
    Dependencies,
28
    DictStringToStringSequenceField,
29
    FieldDefaultFactoryRequest,
30
    FieldDefaultFactoryResult,
31
    GeneratedTargets,
32
    GenerateTargetsRequest,
33
    InvalidFieldException,
34
    InvalidTargetException,
35
    OptionalSingleSourceField,
36
    SequenceField,
37
    SingleSourceField,
38
    SpecialCasedDependencies,
39
    StringField,
40
    StringSequenceField,
41
    Target,
42
    TargetGenerator,
43
)
44
from pants.engine.unions import UnionMembership, UnionRule
11✔
45
from pants.jvm.resolve.coordinate import Coordinate
11✔
46
from pants.jvm.subsystems import JvmSubsystem
11✔
47
from pants.util.docutil import git_url
11✔
48
from pants.util.frozendict import FrozenDict
11✔
49
from pants.util.logging import LogLevel
11✔
50
from pants.util.strutil import bullet_list, help_text, pluralize, softwrap
11✔
51

52
# -----------------------------------------------------------------------------------------------
53
# Generic resolve support fields
54
# -----------------------------------------------------------------------------------------------
55

56

57
class JvmDependenciesField(Dependencies):
11✔
58
    pass
11✔
59

60

61
class JvmResolveField(StringField, AsyncFieldMixin):
11✔
62
    alias = "resolve"
11✔
63
    required = False
11✔
64
    help = help_text(
11✔
65
        """
66
        The resolve from `[jvm].resolves` to use when compiling this target.
67

68
        If not defined, will default to `[jvm].default_resolve`.
69
        """
70
        # TODO: Document expectations for dependencies once we validate that.
71
    )
72

73
    def normalized_value(self, jvm_subsystem: JvmSubsystem) -> str:
11✔
74
        """Get the value after applying the default and validating that the key is recognized."""
75
        resolve = self.value or jvm_subsystem.default_resolve
×
76
        if resolve not in jvm_subsystem.resolves:
×
77
            raise UnrecognizedResolveNamesError(
×
78
                [resolve],
79
                jvm_subsystem.resolves.keys(),
80
                description_of_origin=f"the field `{self.alias}` in the target {self.address}",
81
            )
82
        return resolve
×
83

84

85
class JvmJdkField(StringField):
11✔
86
    alias = "jdk"
11✔
87
    required = False
11✔
88
    help = help_text(
11✔
89
        """
90
        The major version of the JDK that this target should be built with. If not defined,
91
        will default to `[jvm].default_source_jdk`.
92
        """
93
    )
94

95

96
class PrefixedJvmJdkField(JvmJdkField):
11✔
97
    alias = "jvm_jdk"
11✔
98

99

100
class PrefixedJvmResolveField(JvmResolveField):
11✔
101
    alias = "jvm_resolve"
11✔
102

103

104
# -----------------------------------------------------------------------------------------------
105
# Targets that can be called with `./pants run` or `experimental_run_in_sandbox`
106
# -----------------------------------------------------------------------------------------------
107
NO_MAIN_CLASS = "org.pantsbuild.meta.no.main.class"
11✔
108

109

110
class JvmMainClassNameField(StringField):
11✔
111
    alias = "main"
11✔
112
    required = False
11✔
113
    default = None
11✔
114
    help = help_text(
11✔
115
        """
116
        `.`-separated name of the JVM class containing the `main()` method to be called when
117
        executing this target. If not supplied, this will be calculated automatically, either by
118
        inspecting the existing manifest (for 3rd-party JARs), or by inspecting the classes inside
119
        the JAR, looking for a valid `main` method.  If a value cannot be calculated automatically,
120
        you must supply a value for `run` to succeed.
121
        """
122
    )
123

124

125
@dataclass(frozen=True)
11✔
126
class JvmRunnableSourceFieldSet(RunFieldSet):
11✔
127
    run_in_sandbox_behavior = RunInSandboxBehavior.RUN_REQUEST_HERMETIC
11✔
128
    jdk_version: JvmJdkField
11✔
129
    main_class: JvmMainClassNameField
11✔
130

131

132
@dataclass(frozen=True)
11✔
133
class GenericJvmRunRequest:
11✔
134
    """Allows the use of a generic rule to return a `RunRequest` based on the field set."""
135

136
    field_set: JvmRunnableSourceFieldSet
11✔
137

138

139
# -----------------------------------------------------------------------------------------------
140
# `jvm_artifact` targets
141
# -----------------------------------------------------------------------------------------------
142

143
_DEFAULT_PACKAGE_MAPPING_URL = git_url(
11✔
144
    "src/python/pants/jvm/dependency_inference/jvm_artifact_mappings.py"
145
)
146

147

148
class JvmArtifactGroupField(StringField):
11✔
149
    alias = "group"
11✔
150
    required = True
11✔
151
    value: str
11✔
152
    help = help_text(
11✔
153
        """
154
        The 'group' part of a Maven-compatible coordinate to a third-party JAR artifact.
155

156
        For the JAR coordinate `com.google.guava:guava:30.1.1-jre`, the group is `com.google.guava`.
157
        """
158
    )
159

160

161
class JvmArtifactArtifactField(StringField):
11✔
162
    alias = "artifact"
11✔
163
    required = True
11✔
164
    value: str
11✔
165
    help = help_text(
11✔
166
        """
167
        The 'artifact' part of a Maven-compatible coordinate to a third-party JAR artifact.
168

169
        For the JAR coordinate `com.google.guava:guava:30.1.1-jre`, the artifact is `guava`.
170
        """
171
    )
172

173

174
class JvmArtifactVersionField(StringField):
11✔
175
    alias = "version"
11✔
176
    required = True
11✔
177
    value: str
11✔
178
    help = help_text(
11✔
179
        """
180
        The 'version' part of a Maven-compatible coordinate to a third-party JAR artifact.
181

182
        For the JAR coordinate `com.google.guava:guava:30.1.1-jre`, the version is `30.1.1-jre`.
183
        """
184
    )
185

186

187
class JvmArtifactUrlField(StringField):
11✔
188
    alias = "url"
11✔
189
    required = False
11✔
190
    help = help_text(
11✔
191
        """
192
        A URL that points to the location of this artifact.
193

194
        If specified, Pants will not fetch this artifact from default Maven repositories, and
195
        will instead fetch the artifact from this URL. To use default maven
196
        repositories, do not set this value.
197

198
        Note that `file:` URLs are not supported. Instead, use the `jar` field for local
199
        artifacts.
200
        """
201
    )
202

203

204
class JvmArtifactJarSourceField(OptionalSingleSourceField):
11✔
205
    alias = "jar"
11✔
206
    expected_file_extensions = (".jar",)
11✔
207
    help = help_text(
11✔
208
        """
209
        A local JAR file that provides this artifact to the lockfile resolver, instead of a
210
        Maven repository.
211

212
        Path is relative to the BUILD file.
213

214
        Use the `url` field for remote artifacts.
215
        """
216
    )
217

218
    @classmethod
11✔
219
    def compute_value(cls, raw_value: str | None, address: Address) -> str | None:
11✔
220
        value_or_default = super().compute_value(raw_value, address)
1✔
221
        if value_or_default and value_or_default.startswith("file:"):
1✔
222
            raise InvalidFieldException(
×
223
                softwrap(
224
                    f"""
225
                    The `{cls.alias}` field does not support `file:` URLS, but the target
226
                    {address} sets the field to `{value_or_default}`.
227

228
                    Instead, use the `jar` field to specify the relative path to the local jar file.
229
                    """
230
                )
231
            )
232
        return value_or_default
1✔
233

234

235
class JvmArtifactPackagesField(StringSequenceField):
11✔
236
    alias = "packages"
11✔
237
    help = help_text(
11✔
238
        f"""
239
        The JVM packages this artifact provides for the purposes of dependency inference.
240

241
        For example, the JVM artifact `junit:junit` might provide `["org.junit.**"]`.
242

243
        Usually you can leave this field off. If unspecified, Pants will fall back to the
244
        `[java-infer].third_party_import_mapping`, then to a built in mapping
245
        ({_DEFAULT_PACKAGE_MAPPING_URL}), and then finally it will default to
246
        the normalized `group` of the artifact. For example, in the absence of any other mapping
247
        the artifact `io.confluent:common-config` would default to providing
248
        `["io.confluent.**"]`.
249

250
        The package path may be made recursive to match symbols in subpackages
251
        by adding `.**` to the end of the package path. For example, specify `["org.junit.**"]`
252
        to infer a dependency on the artifact for any file importing a symbol from `org.junit` or
253
        its subpackages.
254
        """
255
    )
256

257

258
class JvmArtifactForceVersionField(BoolField):
11✔
259
    alias = "force_version"
11✔
260
    default = False
11✔
261
    help = help_text(
11✔
262
        """
263
        Force artifact version during resolution.
264

265
        If set, pants will pass `--force-version` argument to `coursier fetch` for this artifact.
266
        """
267
    )
268

269

270
class JvmProvidesTypesField(StringSequenceField):
11✔
271
    alias = "experimental_provides_types"
11✔
272
    help = help_text(
11✔
273
        """
274
        Signals that the specified types should be fulfilled by these source files during
275
        dependency inference.
276

277
        This allows for specific types within packages that are otherwise inferred as
278
        belonging to `jvm_artifact` targets to be unambiguously inferred as belonging
279
        to this first-party source.
280

281
        If a given type is defined, at least one source file captured by this target must
282
        actually provide that symbol.
283
        """
284
    )
285

286

287
@dataclass(frozen=True)
11✔
288
class JvmArtifactExclusion:
11✔
289
    alias: ClassVar[str] = "jvm_exclude"
11✔
290
    help: ClassVar[str | Callable[[], str]] = help_text(
11✔
291
        """
292
        Exclude the given `artifact` and `group`, or all artifacts from the given `group`.
293
        """
294
    )
295

296
    group: str
11✔
297
    artifact: str | None = None
11✔
298

299
    def validate(self, _: Address) -> set[str]:
11✔
300
        return set()
1✔
301

302
    def to_coord_str(self) -> str:
11✔
303
        result = self.group
1✔
304
        if self.artifact:
1✔
305
            result += f":{self.artifact}"
×
306
        else:
307
            result += ":*"
1✔
308
        return result
1✔
309

310

311
def _jvm_artifact_exclusions_field_help(
11✔
312
    supported_exclusions: Callable[[], Iterable[type[JvmArtifactExclusion]]],
313
) -> str | Callable[[], str]:
314
    return help_text(
11✔
315
        lambda: f"""
316
        A list of exclusions for unversioned coordinates that should be excluded
317
        as dependencies when this artifact is resolved.
318

319
        This does not prevent this artifact from being included in the resolve as a dependency
320
        of other artifacts that depend on it, and is currently intended as a way to resolve
321
        version conflicts in complex resolves.
322

323
        Supported exclusions are:
324
        {bullet_list(f"`{exclusion.alias}`: {exclusion.help}" for exclusion in supported_exclusions())}
325
        """
326
    )
327

328

329
class JvmArtifactExclusionsField(SequenceField[JvmArtifactExclusion]):
11✔
330
    alias = "exclusions"
11✔
331
    help = _jvm_artifact_exclusions_field_help(
11✔
332
        lambda: JvmArtifactExclusionsField.supported_exclusion_types
333
    )
334

335
    supported_exclusion_types: ClassVar[tuple[type[JvmArtifactExclusion], ...]] = (
11✔
336
        JvmArtifactExclusion,
337
    )
338
    expected_element_type = JvmArtifactExclusion
11✔
339
    expected_type_description = "an iterable of JvmArtifactExclusionRule"
11✔
340

341
    @classmethod
11✔
342
    def compute_value(
11✔
343
        cls, raw_value: Iterable[JvmArtifactExclusion] | None, address: Address
344
    ) -> tuple[JvmArtifactExclusion, ...] | None:
345
        computed_value = super().compute_value(raw_value, address)
1✔
346

347
        if computed_value:
1✔
348
            errors: list[str] = []
1✔
349
            for exclusion_rule in computed_value:
1✔
350
                err = exclusion_rule.validate(address)
1✔
351
                if err:
1✔
352
                    errors.extend(err)
×
353

354
            if errors:
1✔
355
                raise InvalidFieldException(
×
356
                    softwrap(
357
                        f"""
358
                        Invalid value for `{JvmArtifactExclusionsField.alias}` field at target
359
                        {address}. Found following errors:
360

361
                        {bullet_list(errors)}
362
                        """
363
                    )
364
                )
365
        return computed_value
1✔
366

367

368
class JvmArtifactResolveField(JvmResolveField):
11✔
369
    help = help_text(
11✔
370
        """
371
        The resolve from `[jvm].resolves` that this artifact should be included in.
372

373
        If not defined, will default to `[jvm].default_resolve`.
374

375
        When generating a lockfile for a particular resolve via the `coursier-resolve` goal,
376
        it will include all artifacts that are declared compatible with that resolve. First-party
377
        targets like `java_source` and `scala_source` also declare which resolve they use
378
        via the `resolve` field; so, for your first-party code to use
379
        a particular `jvm_artifact` target, that artifact must be included in the resolve
380
        used by that code.
381
        """
382
    )
383

384

385
@dataclass(frozen=True)
11✔
386
class JvmArtifactFieldSet(JvmRunnableSourceFieldSet):
11✔
387
    group: JvmArtifactGroupField
11✔
388
    artifact: JvmArtifactArtifactField
11✔
389
    version: JvmArtifactVersionField
11✔
390
    packages: JvmArtifactPackagesField
11✔
391
    url: JvmArtifactUrlField
11✔
392
    force_version: JvmArtifactForceVersionField
11✔
393

394
    required_fields = (
11✔
395
        JvmArtifactGroupField,
396
        JvmArtifactArtifactField,
397
        JvmArtifactVersionField,
398
        JvmArtifactPackagesField,
399
        JvmArtifactForceVersionField,
400
    )
401

402

403
class JvmArtifactTarget(Target):
11✔
404
    alias = "jvm_artifact"
11✔
405
    core_fields = (
11✔
406
        *COMMON_TARGET_FIELDS,
407
        *JvmArtifactFieldSet.required_fields,
408
        JvmArtifactUrlField,  # TODO: should `JvmArtifactFieldSet` have an `all_fields` field?
409
        JvmArtifactJarSourceField,
410
        JvmArtifactResolveField,
411
        JvmArtifactExclusionsField,
412
        JvmJdkField,
413
        JvmMainClassNameField,
414
    )
415
    help = help_text(
11✔
416
        """
417
        A third-party JVM artifact, as identified by its Maven-compatible coordinate.
418

419
        That is, an artifact identified by its `group`, `artifact`, and `version` components.
420

421
        Each artifact is associated with one or more resolves (a logical name you give to a
422
        lockfile). For this artifact to be used by your first-party code, it must be
423
        associated with the resolve(s) used by that code. See the `resolve` field.
424
        """
425
    )
426

427
    def validate(self) -> None:
11✔
428
        if self[JvmArtifactJarSourceField].value and self[JvmArtifactUrlField].value:
1✔
429
            raise InvalidTargetException(
×
430
                f"You cannot specify both the `url` and `jar` fields, but both were set on the "
431
                f"`{self.alias}` target {self.address}."
432
            )
433

434

435
# -----------------------------------------------------------------------------------------------
436
# Generate `jvm_artifact` targets from pom.xml
437
# -----------------------------------------------------------------------------------------------
438

439

440
class PomXmlSourceField(SingleSourceField):
11✔
441
    default = "pom.xml"
11✔
442
    required = False
11✔
443

444

445
class JvmArtifactsPackageMappingField(DictStringToStringSequenceField):
11✔
446
    alias = "package_mapping"
11✔
447
    help = help_text(
11✔
448
        f"""
449
        A mapping of jvm artifacts to a list of the packages they provide.
450

451
        For example, `{{"com.google.guava:guava": ["com.google.common.**"]}}`.
452

453
        Any unspecified jvm artifacts will use a default. See the
454
        `{JvmArtifactPackagesField.alias}` field from the `{JvmArtifactTarget.alias}`
455
        target for more information.
456
        """
457
    )
458
    value: FrozenDict[str, tuple[str, ...]]
11✔
459
    default: ClassVar[FrozenDict[str, tuple[str, ...]] | None] = FrozenDict()
11✔
460

461
    @classmethod
11✔
462
    def compute_value(  # type: ignore[override]
11✔
463
        cls, raw_value: dict[str, Iterable[str]], address: Address
464
    ) -> FrozenDict[tuple[str, str], tuple[str, ...]]:
465
        value_or_default = super().compute_value(raw_value, address)
×
466
        assert value_or_default is not None
×
467
        return FrozenDict(
×
468
            {
469
                cls._parse_coord(coord): tuple(packages)
470
                for coord, packages in value_or_default.items()
471
            }
472
        )
473

474
    @classmethod
11✔
475
    def _parse_coord(cls, coord: str) -> tuple[str, str]:
11✔
476
        group, artifact = coord.split(":")
×
477
        return group, artifact
×
478

479

480
class JvmArtifactsTargetGenerator(TargetGenerator):
11✔
481
    alias = "jvm_artifacts"
11✔
482
    core_fields = (
11✔
483
        PomXmlSourceField,
484
        JvmArtifactsPackageMappingField,
485
        *COMMON_TARGET_FIELDS,
486
    )
487
    generated_target_cls = JvmArtifactTarget
11✔
488
    copied_fields = COMMON_TARGET_FIELDS
11✔
489
    moved_fields = (JvmArtifactResolveField,)
11✔
490
    help = help_text(
11✔
491
        """
492
        Generate a `jvm_artifact` target for each dependency in pom.xml file.
493
        """
494
    )
495

496

497
class GenerateFromPomXmlRequest(GenerateTargetsRequest):
11✔
498
    generate_from = JvmArtifactsTargetGenerator
11✔
499

500

501
@rule(
11✔
502
    desc=("Generate `jvm_artifact` targets from pom.xml"),
503
    level=LogLevel.DEBUG,
504
)
505
async def generate_from_pom_xml(
11✔
506
    request: GenerateFromPomXmlRequest,
507
    union_membership: UnionMembership,
508
) -> GeneratedTargets:
509
    generator = request.generator
×
510
    pom_xml = await determine_source_files(SourceFilesRequest([generator[PomXmlSourceField]]))
×
511
    files = await get_digest_contents(pom_xml.snapshot.digest)
×
512
    if not files:
×
513
        raise FileNotFoundError(f"pom.xml not found: {generator[PomXmlSourceField].value}")
×
514

515
    mapping = request.generator[JvmArtifactsPackageMappingField].value
×
516
    coordinates = parse_pom_xml(files[0].content, pom_xml_path=pom_xml.snapshot.files[0])
×
517
    targets = (
×
518
        JvmArtifactTarget(
519
            unhydrated_values={
520
                "group": coord.group,
521
                "artifact": coord.artifact,
522
                "version": coord.version,
523
                "packages": mapping.get((coord.group, coord.artifact)),
524
                **request.template,
525
            },
526
            address=request.template_address.create_generated(coord.artifact),
527
        )
528
        for coord in coordinates
529
    )
530
    return GeneratedTargets(request.generator, targets)
×
531

532

533
def parse_pom_xml(content: bytes, pom_xml_path: str) -> Iterator[Coordinate]:
11✔
534
    root = ET.fromstring(content.decode("utf-8"))
×
535
    match = re.match(r"^(\{.*\})project$", root.tag)
×
536
    if not match:
×
537
        raise ValueError(
×
538
            f"Unexpected root tag `{root.tag}` in {pom_xml_path}, expected tag `project`"
539
        )
540

541
    namespace = match.group(1)
×
542
    for dependency in root.iter(f"{namespace}dependency"):
×
543
        yield Coordinate(
×
544
            group=get_child_text(dependency, f"{namespace}groupId"),
545
            artifact=get_child_text(dependency, f"{namespace}artifactId"),
546
            version=get_child_text(dependency, f"{namespace}version"),
547
        )
548

549

550
def get_child_text(parent: ET.Element, child: str) -> str:
11✔
551
    tag = parent.find(child)
×
552
    if tag is None:
×
553
        raise ValueError(f"missing element: {child}")
×
554
    text = tag.text
×
555
    if text is None:
×
556
        raise ValueError(f"empty element: {child}")
×
557
    return text
×
558

559

560
# -----------------------------------------------------------------------------------------------
561
# JUnit test support field(s)
562
# -----------------------------------------------------------------------------------------------
563

564

565
class JunitTestSourceField(SingleSourceField, metaclass=ABCMeta):
11✔
566
    """A marker that indicates that a source field represents a JUnit test."""
567

568

569
class JunitTestTimeoutField(TestTimeoutField):
11✔
570
    pass
11✔
571

572

573
class JunitTestExtraEnvVarsField(TestExtraEnvVarsField):
11✔
574
    pass
11✔
575

576

577
# -----------------------------------------------------------------------------------------------
578
# JAR support fields
579
# -----------------------------------------------------------------------------------------------
580

581

582
class JvmRequiredMainClassNameField(JvmMainClassNameField):
11✔
583
    required = True
11✔
584
    default = None
11✔
585
    help = help_text(
11✔
586
        """
587
        `.`-separated name of the JVM class containing the `main()` method to be called when
588
        executing this JAR.
589
        """
590
    )
591

592

593
class JvmShadingRule(ABC):
11✔
594
    """Base class for defining JAR shading rules as valid aliases in BUILD files.
595

596
    Subclasses need to provide with an `alias` and a `help` message. The `alias` represents
597
    the name that will be used in BUILD files to instantiate the given subclass.
598

599
    Set the `help` class property with a description, which will be used in `./pants help`. For the
600
    best rendering, use soft wrapping (e.g. implicit string concatenation) within paragraphs, but
601
    hard wrapping (`\n`) to separate distinct paragraphs and/or lists.
602
    """
603

604
    alias: ClassVar[str]
11✔
605
    help: ClassVar[str | Callable[[], str]]
11✔
606

607
    @abstractmethod
11✔
608
    def encode(self) -> str:
11✔
609
        pass
×
610

611
    @abstractmethod
11✔
612
    def validate(self) -> set[str]:
11✔
613
        pass
×
614

615
    @staticmethod
11✔
616
    def _validate_field(value: str, *, name: str, invalid_chars: str) -> set[str]:
11✔
617
        errors = []
1✔
618
        for ch in invalid_chars:
1✔
619
            if ch in value:
1✔
UNCOV
620
                errors.append(f"`{name}` can not contain the character `{ch}`.")
×
621
        return set(errors)
1✔
622

623
    def __repr__(self) -> str:
11✔
624
        fields = [f"{fld.name}={repr(getattr(self, fld.name))}" for fld in dataclasses.fields(self)]  # type: ignore[arg-type]
×
625
        return f"{self.alias}({', '.join(fields)})"
×
626

627

628
@dataclass(frozen=True, repr=False)
11✔
629
class JvmShadingRenameRule(JvmShadingRule):
11✔
630
    alias = "shading_rename"
11✔
631
    help = "Renames all occurrences of the given `pattern` by the `replacement`."
11✔
632

633
    pattern: str
11✔
634
    replacement: str
11✔
635

636
    def encode(self) -> str:
11✔
637
        return f"rule {self.pattern} {self.replacement}"
×
638

639
    def validate(self) -> set[str]:
11✔
640
        errors: list[str] = []
1✔
641
        errors.extend(
1✔
642
            JvmShadingRule._validate_field(self.pattern, name="pattern", invalid_chars="/")
643
        )
644
        errors.extend(
1✔
645
            JvmShadingRule._validate_field(self.replacement, name="replacement", invalid_chars="/")
646
        )
647
        return set(errors)
1✔
648

649

650
@dataclass(frozen=True, repr=False)
11✔
651
class JvmShadingRelocateRule(JvmShadingRule):
11✔
652
    alias = "shading_relocate"
11✔
653
    help = help_text(
11✔
654
        """
655
        Relocates the classes under the given `package` into the new package name.
656
        The default target package is `__shaded_by_pants__` if none provided in
657
        the `into` parameter.
658
        """
659
    )
660

661
    package: str
11✔
662
    into: str | None = None
11✔
663

664
    def encode(self) -> str:
11✔
665
        if not self.into:
×
666
            target_suffix = "__shaded_by_pants__"
×
667
        else:
668
            target_suffix = self.into
×
669
        return f"rule {self.package}.** {target_suffix}.@1"
×
670

671
    def validate(self) -> set[str]:
11✔
672
        errors: list[str] = []
1✔
673
        errors.extend(
1✔
674
            JvmShadingRule._validate_field(self.package, name="package", invalid_chars="/*")
675
        )
676
        if self.into:
1✔
677
            errors.extend(
1✔
678
                JvmShadingRule._validate_field(self.into, name="into", invalid_chars="/*")
679
            )
680
        return set(errors)
1✔
681

682

683
@dataclass(frozen=True, repr=False)
11✔
684
class JvmShadingZapRule(JvmShadingRule):
11✔
685
    alias = "shading_zap"
11✔
686
    help = "Removes from the final artifact the occurrences of the `pattern`."
11✔
687

688
    pattern: str
11✔
689

690
    def encode(self) -> str:
11✔
691
        return f"zap {self.pattern}"
×
692

693
    def validate(self) -> set[str]:
11✔
UNCOV
694
        return JvmShadingRule._validate_field(self.pattern, name="pattern", invalid_chars="/")
×
695

696

697
@dataclass(frozen=True, repr=False)
11✔
698
class JvmShadingKeepRule(JvmShadingRule):
11✔
699
    alias = "shading_keep"
11✔
700
    help = help_text(
11✔
701
        """
702
        Keeps in the final artifact the occurrences of the `pattern`
703
        (and removes anything else).
704
        """
705
    )
706

707
    pattern: str
11✔
708

709
    def encode(self) -> str:
11✔
710
        return f"keep {self.pattern}"
×
711

712
    def validate(self) -> set[str]:
11✔
UNCOV
713
        return JvmShadingRule._validate_field(self.pattern, name="pattern", invalid_chars="/")
×
714

715

716
JVM_SHADING_RULE_TYPES: list[type[JvmShadingRule]] = [
11✔
717
    JvmShadingRelocateRule,
718
    JvmShadingRenameRule,
719
    JvmShadingZapRule,
720
    JvmShadingKeepRule,
721
]
722

723

724
def _shading_rules_field_help(intro: str) -> str:
11✔
725
    return softwrap(
11✔
726
        f"""
727
        {intro}
728

729
        There are {pluralize(len(JVM_SHADING_RULE_TYPES), "possible shading rule")} available,
730
        which are as follows:
731
        {bullet_list([f"`{rule.alias}`: {rule.help}" for rule in JVM_SHADING_RULE_TYPES])}
732

733
        When defining shading rules, just add them in this field using the previously listed rule
734
        alias and passing along the required parameters.
735
        """
736
    )
737

738

739
def _shading_validate_rules(shading_rules: Iterable[JvmShadingRule]) -> set[str]:
11✔
740
    validation_errors = []
1✔
741
    for shading_rule in shading_rules:
1✔
742
        found_errors = shading_rule.validate()
1✔
743
        if found_errors:
1✔
UNCOV
744
            validation_errors.append(
×
745
                "\n".join(
746
                    [
747
                        f"In rule `{shading_rule.alias}`:",
748
                        bullet_list(found_errors),
749
                        "",
750
                    ]
751
                )
752
            )
753
    return set(validation_errors)
1✔
754

755

756
class JvmShadingRulesField(SequenceField[JvmShadingRule], metaclass=ABCMeta):
11✔
757
    alias = "shading_rules"
11✔
758
    required = False
11✔
759
    expected_element_type = JvmShadingRule
11✔
760
    expected_type_description = "an iterable of JvmShadingRule"
11✔
761

762
    @classmethod
11✔
763
    def compute_value(
11✔
764
        cls, raw_value: Iterable[JvmShadingRule] | None, address: Address
765
    ) -> tuple[JvmShadingRule, ...] | None:
766
        computed_value = super().compute_value(raw_value, address)
×
767

768
        if computed_value:
×
769
            validation_errors = _shading_validate_rules(computed_value)
×
770
            if validation_errors:
×
771
                raise InvalidFieldException(
×
772
                    "\n".join(
773
                        [
774
                            f"Invalid shading rules assigned to `{cls.alias}` field in target {address}:\n",
775
                            *validation_errors,
776
                        ]
777
                    )
778
                )
779

780
        return computed_value
×
781

782

783
# -----------------------------------------------------------------------------------------------
784
# `deploy_jar` target
785
# -----------------------------------------------------------------------------------------------
786

787

788
@dataclass(frozen=True)
11✔
789
class DeployJarDuplicateRule:
11✔
790
    alias: ClassVar[str] = "duplicate_rule"
11✔
791
    valid_actions: ClassVar[tuple[str, ...]] = ("skip", "replace", "concat", "concat_text", "throw")
11✔
792

793
    pattern: str
11✔
794
    action: str
11✔
795

796
    def validate(self) -> str | None:
11✔
797
        if self.action not in DeployJarDuplicateRule.valid_actions:
×
798
            return softwrap(
×
799
                f"""
800
                Value '{self.action}' for `action` associated with pattern
801
                '{self.pattern}' is not valid.
802

803
                It must be one of {list(DeployJarDuplicateRule.valid_actions)}.
804
                """
805
            )
806
        return None
×
807

808
    def __repr__(self) -> str:
11✔
809
        return f"{self.alias}(pattern='{self.pattern}', action='{self.action}')"
×
810

811

812
class DeployJarDuplicatePolicyField(SequenceField[DeployJarDuplicateRule]):
11✔
813
    alias = "duplicate_policy"
11✔
814
    help = help_text(
11✔
815
        f"""
816
        A list of the rules to apply when duplicate file entries are found in the final
817
        assembled JAR file.
818

819
        When defining a duplicate policy, just add `duplicate_rule` directives to this
820
        field as follows:
821

822
        Example:
823

824
            duplicate_policy=[
825
                duplicate_rule(pattern="^META-INF/services", action="concat_text"),
826
                duplicate_rule(pattern="^reference\\.conf", action="concat_text"),
827
                duplicate_rule(pattern="^org/apache/commons", action="throw"),
828
            ]
829

830
        Where:
831

832
        * The `pattern` field is treated as a regular expression
833
        * The `action` field must be one of `{list(DeployJarDuplicateRule.valid_actions)}`.
834

835
        Note that the order in which the rules are listed is relevant.
836
        """
837
    )
838
    required = False
11✔
839

840
    expected_element_type = DeployJarDuplicateRule
11✔
841
    expected_type_description = "a list of JAR duplicate rules"
11✔
842

843
    default = (
11✔
844
        DeployJarDuplicateRule(pattern="^META-INF/services/", action="concat_text"),
845
        DeployJarDuplicateRule(pattern="^META-INF/LICENSE", action="skip"),
846
    )
847

848
    @classmethod
11✔
849
    def compute_value(
11✔
850
        cls, raw_value: Iterable[DeployJarDuplicateRule] | None, address: Address
851
    ) -> tuple[DeployJarDuplicateRule, ...] | None:
852
        value = super().compute_value(raw_value, address)
×
853
        if value:
×
854
            errors = []
×
855
            for duplicate_rule in value:
×
856
                err = duplicate_rule.validate()
×
857
                if err:
×
858
                    errors.append(err)
×
859

860
            if errors:
×
861
                raise InvalidFieldException(
×
862
                    softwrap(
863
                        f"""
864
                        Invalid value for `{DeployJarDuplicatePolicyField.alias}` field at target:
865
                        {address}. Found following errors:
866

867
                        {bullet_list(errors)}
868
                        """
869
                    )
870
                )
871
        return value
×
872

873
    def value_or_default(self) -> tuple[DeployJarDuplicateRule, ...]:
11✔
874
        if self.value is not None:
×
875
            return self.value
×
876
        return self.default
×
877

878

879
class DeployJarShadingRulesField(JvmShadingRulesField):
11✔
880
    help = _shading_rules_field_help("Shading rules to be applied to the final JAR artifact.")
11✔
881

882

883
class DeployJarExcludeFilesField(StringSequenceField):
11✔
884
    alias = "exclude_files"
11✔
885
    help = help_text(
11✔
886
        """
887
        A list of patterns to exclude from the final jar.
888
        """
889
    )
890

891

892
class DeployJarTarget(Target):
11✔
893
    alias = "deploy_jar"
11✔
894
    core_fields = (
11✔
895
        *COMMON_TARGET_FIELDS,
896
        RestartableField,
897
        OutputPathField,
898
        JvmDependenciesField,
899
        JvmRequiredMainClassNameField,
900
        JvmJdkField,
901
        JvmResolveField,
902
        DeployJarDuplicatePolicyField,
903
        DeployJarShadingRulesField,
904
        DeployJarExcludeFilesField,
905
    )
906
    help = help_text(
11✔
907
        """
908
        A `jar` file with first and third-party code bundled for deploys.
909

910
        The JAR will contain class files for both first-party code and
911
        third-party dependencies, all in a common directory structure.
912
        """
913
    )
914

915

916
# -----------------------------------------------------------------------------------------------
917
# `jvm_war` targets
918
# -----------------------------------------------------------------------------------------------
919

920

921
class JvmWarDependenciesField(Dependencies):
11✔
922
    pass
11✔
923

924

925
class JvmWarDescriptorAddressField(SingleSourceField):
11✔
926
    alias = "descriptor"
11✔
927
    default = "web.xml"
11✔
928
    help = "Path to a file containing the descriptor (i.e., `web.xml`) for this WAR file. Defaults to `web.xml`."
11✔
929

930

931
class JvmWarContentField(SpecialCasedDependencies):
11✔
932
    alias = "content"
11✔
933
    help = help_text(
11✔
934
        """
935
        A list of addresses to `resources` and `files` targets with content to place in the
936
        document root of this WAR file.
937
        """
938
    )
939

940

941
class JvmWarShadingRulesField(JvmShadingRulesField):
11✔
942
    help = _shading_rules_field_help(
11✔
943
        "Shading rules to be applied to the individual JAR artifacts embedded in the `WEB-INF/lib` folder."
944
    )
945

946

947
class JvmWarTarget(Target):
11✔
948
    alias = "jvm_war"
11✔
949
    core_fields = (
11✔
950
        *COMMON_TARGET_FIELDS,
951
        JvmResolveField,
952
        JvmWarContentField,
953
        JvmWarDependenciesField,
954
        JvmWarDescriptorAddressField,
955
        JvmWarShadingRulesField,
956
        OutputPathField,
957
    )
958
    help = help_text(
11✔
959
        """
960
        A JSR 154 "web application archive" (or "war") with first-party and third-party code bundled for
961
        deploys in Java Servlet containers.
962
        """
963
    )
964

965

966
# -----------------------------------------------------------------------------------------------
967
# Dynamic Field defaults
968
# -----------------------------------------------------------------------------------------------#
969

970

971
class JvmResolveFieldDefaultFactoryRequest(FieldDefaultFactoryRequest):
11✔
972
    field_type = JvmResolveField
11✔
973

974

975
@rule
11✔
976
async def jvm_resolve_field_default_factory(
11✔
977
    request: JvmResolveFieldDefaultFactoryRequest,
978
    jvm: JvmSubsystem,
979
) -> FieldDefaultFactoryResult:
980
    return FieldDefaultFactoryResult(lambda f: f.normalized_value(jvm))
×
981

982

983
def rules():
11✔
984
    return [
9✔
985
        *collect_rules(),
986
        UnionRule(GenerateTargetsRequest, GenerateFromPomXmlRequest),
987
        UnionRule(FieldDefaultFactoryRequest, JvmResolveFieldDefaultFactoryRequest),
988
    ]
989

990

991
def build_file_aliases():
11✔
992
    return BuildFileAliases(
×
993
        objects={
994
            JvmArtifactExclusion.alias: JvmArtifactExclusion,
995
            DeployJarDuplicateRule.alias: DeployJarDuplicateRule,
996
            **{rule.alias: rule for rule in JVM_SHADING_RULE_TYPES},
997
        }
998
    )
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