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

pantsbuild / pants / 18987624565

31 Oct 2025 11:28PM UTC coverage: 80.299% (+0.02%) from 80.275%
18987624565

push

github

web-flow
Allow setting Python resolve interpreter_constraints as defaults for targets (#22676)

Closes #22574 

This PR is intended to solve a long-felt annoyance of mine when working
in repos with multiple Python resolves, which is having to configure
resolve interpreter constraints and source interpreter constraints
separately. It adds a new option,
`[python].default_to_resolve_interpreter_constraints`, which when set to
true, tells Pants to use the interpreter constraints of the resolve,
rather than the global interpreter constraints, if no interpreter
constraints are provided. If resolves are not enabled or no interpreter
constraints are set for the resolve, it still falls back to the global
default.

71 of 95 new or added lines in 17 files covered. (74.74%)

2 existing lines in 2 files now uncovered.

77993 of 97128 relevant lines covered (80.3%)

3.35 hits per line

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

58.14
/src/python/pants/backend/python/lint/black/rules.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
10✔
5

6
from typing import cast
10✔
7

8
from pants.backend.python.lint.black.subsystem import Black, BlackFieldSet
10✔
9
from pants.backend.python.subsystems.setup import PythonSetup
10✔
10
from pants.backend.python.util_rules import pex
10✔
11
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
10✔
12
from pants.backend.python.util_rules.pex import VenvPexProcess, create_venv_pex
10✔
13
from pants.core.goals.fmt import AbstractFmtRequest, FmtResult, FmtTargetsRequest, Partitions
10✔
14
from pants.core.util_rules.config_files import find_config_file
10✔
15
from pants.engine.fs import MergeDigests
10✔
16
from pants.engine.intrinsics import merge_digests
10✔
17
from pants.engine.process import execute_process_or_raise
10✔
18
from pants.engine.rules import collect_rules, concurrently, implicitly, rule
10✔
19
from pants.util.logging import LogLevel
10✔
20
from pants.util.strutil import pluralize, softwrap
10✔
21

22

23
class BlackRequest(FmtTargetsRequest):
10✔
24
    field_set_type = BlackFieldSet
10✔
25
    tool_subsystem = Black  # type: ignore[assignment]
10✔
26

27

28
async def _run_black(
10✔
29
    request: AbstractFmtRequest.Batch,
30
    black: Black,
31
    interpreter_constraints: InterpreterConstraints,
32
) -> FmtResult:
33
    black_pex_get = create_venv_pex(
×
34
        **implicitly(black.to_pex_request(interpreter_constraints=interpreter_constraints))
35
    )
36
    config_files_get = find_config_file(black.config_request(request.snapshot.dirs))
×
37
    black_pex, config_files = await concurrently(black_pex_get, config_files_get)
×
38

39
    input_digest = await merge_digests(
×
40
        MergeDigests((request.snapshot.digest, config_files.snapshot.digest))
41
    )
42
    result = await execute_process_or_raise(
×
43
        **implicitly(
44
            VenvPexProcess(
45
                black_pex,
46
                argv=(
47
                    *(("--config", black.config) if black.config else ()),
48
                    "-W",
49
                    "{pants_concurrency}",
50
                    *black.args,
51
                    *request.files,
52
                ),
53
                input_digest=input_digest,
54
                output_files=request.files,
55
                # Note - the cache directory is not used by Pants,
56
                # and we pass through a temporary directory to neutralize
57
                # Black's caching behavior in favor of Pants' caching.
58
                extra_env={"BLACK_CACHE_DIR": "__pants_black_cache"},
59
                concurrency_available=len(request.files),
60
                description=f"Run Black on {pluralize(len(request.files), 'file')}.",
61
                level=LogLevel.DEBUG,
62
            )
63
        )
64
    )
65
    return await FmtResult.create(request, result)
×
66

67

68
@rule
10✔
69
async def partition_black(
10✔
70
    request: BlackRequest.PartitionRequest, black: Black, python_setup: PythonSetup
71
) -> Partitions:
72
    if black.skip:
×
73
        return Partitions()
×
74

75
    # Black requires 3.6+ but uses the typed-ast library to work with 2.7, 3.4, 3.5, 3.6, and 3.7.
76
    # However, typed-ast does not understand 3.8+, so instead we must run Black with Python 3.8+
77
    # when relevant. We only do this if <3.8 can't be used, as we don't want a loose requirement
78
    # like `>=3.6` to result in requiring Python 3.8, which would error if 3.8 is not installed on
79
    # the machine.
80
    tool_interpreter_constraints = black.interpreter_constraints
×
81
    if black.options.is_default("interpreter_constraints"):
×
82
        try:
×
83
            # Don't compute this unless we have to, since it might fail.
NEW
84
            all_interpreter_constraints = InterpreterConstraints.create_from_field_sets(
×
85
                request.field_sets, python_setup
86
            )
87
        except ValueError:
×
88
            raise ValueError(
×
89
                softwrap(
90
                    """
91
                    Could not compute an interpreter to run Black on, due to conflicting requirements
92
                    in the repo.
93

94
                    Please set `[black].interpreter_constraints` explicitly in pants.toml to a
95
                    suitable interpreter.
96
                    """
97
                )
98
            )
99
        if all_interpreter_constraints.requires_python38_or_newer(
×
100
            python_setup.interpreter_versions_universe
101
        ):
102
            tool_interpreter_constraints = all_interpreter_constraints
×
103

104
    return Partitions.single_partition(
×
105
        (field_set.source.file_path for field_set in request.field_sets),
106
        metadata=tool_interpreter_constraints,
107
    )
108

109

110
@rule(desc="Format with Black", level=LogLevel.DEBUG)
10✔
111
async def black_fmt(request: BlackRequest.Batch, black: Black) -> FmtResult:
10✔
112
    return await _run_black(
×
113
        request, black, cast(InterpreterConstraints, request.partition_metadata)
114
    )
115

116

117
def rules():
10✔
118
    return (
4✔
119
        *collect_rules(),
120
        *BlackRequest.rules(),
121
        *pex.rules(),
122
    )
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