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

pantsbuild / pants / 20147226056

11 Dec 2025 08:58PM UTC coverage: 78.827% (-1.5%) from 80.293%
20147226056

push

github

web-flow
Forwarded the `style` and `complete-platform` args from pants.toml to PEX (#22910)

## Context

After Apple switched to the `arm64` architecture, some package
publishers stopped releasing `x86_64` variants of their packages for
`darwin`. As a result, generating a universal lockfile now fails because
no single package version is compatible with both `x86_64` and `arm64`
on `darwin`.

The solution is to use the `--style` and `--complete-platform` flags
with PEX. For example:
```
pex3 lock create \
    --style strict \
    --complete-platform 3rdparty/platforms/manylinux_2_28_aarch64.json \
    --complete-platform 3rdparty/platforms/macosx_26_0_arm64.json \
    -r 3rdparty/python/requirements_pyarrow.txt \
    -o python-pyarrow.lock
```

See the Slack discussion here:
https://pantsbuild.slack.com/archives/C046T6T9U/p1760098582461759

## Reproduction

* `BUILD`
```
python_requirement(
    name="awswrangler",
    requirements=["awswrangler==3.12.1"],
    resolve="awswrangler",
)
```
* Run `pants generate-lockfiles --resolve=awswrangler` on macOS with an
`arm64` CPU
```
pip: ERROR: Cannot install awswrangler==3.12.1 because these package versions have conflicting dependencies.
pip: ERROR: ResolutionImpossible: for help visit https://pip.pypa.io/en/latest/topics/dependency-resolution/#dealing-with-dependency-conflicts
pip:  
pip:  The conflict is caused by:
pip:      awswrangler 3.12.1 depends on pyarrow<18.0.0 and >=8.0.0; sys_platform == "darwin" and platform_machine == "x86_64"
pip:      awswrangler 3.12.1 depends on pyarrow<21.0.0 and >=18.0.0; sys_platform != "darwin" or platform_machine != "x86_64"
pip:  
pip:  Additionally, some packages in these conflicts have no matching distributions available for your environment:
pip:      pyarrow
pip:  
pip:  To fix this you could try to:
pip:  1. loosen the range of package versions you've specified
pip:  2. remove package versions to allow pip to attempt to solve the dependency conflict
```

## Implementation
... (continued)

77 of 100 new or added lines in 6 files covered. (77.0%)

868 existing lines in 42 files now uncovered.

74471 of 94474 relevant lines covered (78.83%)

3.18 hits per line

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

0.0
/src/python/pants/backend/python/mixed_interpreter_constraints/py_constraints.py
1
# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

UNCOV
4
import csv
×
UNCOV
5
import logging
×
UNCOV
6
from collections import defaultdict
×
UNCOV
7
from textwrap import fill, indent
×
8

UNCOV
9
from pants.backend.project_info.dependents import DependentsRequest, find_dependents
×
UNCOV
10
from pants.backend.python.subsystems.setup import PythonSetup
×
UNCOV
11
from pants.backend.python.target_types import InterpreterConstraintsField
×
UNCOV
12
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
×
UNCOV
13
from pants.engine.addresses import Addresses
×
UNCOV
14
from pants.engine.console import Console
×
UNCOV
15
from pants.engine.goal import Goal, GoalSubsystem, Outputting
×
UNCOV
16
from pants.engine.internals.graph import find_all_targets
×
UNCOV
17
from pants.engine.internals.graph import transitive_targets as transitive_targets_get
×
UNCOV
18
from pants.engine.rules import collect_rules, concurrently, goal_rule, implicitly
×
UNCOV
19
from pants.engine.target import RegisteredTargetTypes, TransitiveTargetsRequest
×
UNCOV
20
from pants.engine.unions import UnionMembership
×
UNCOV
21
from pants.option.option_types import BoolOption
×
UNCOV
22
from pants.util.docutil import bin_name
×
UNCOV
23
from pants.util.strutil import softwrap
×
24

UNCOV
25
logger = logging.getLogger(__name__)
×
26

27

UNCOV
28
class PyConstraintsSubsystem(Outputting, GoalSubsystem):
×
UNCOV
29
    name = "py-constraints"
×
UNCOV
30
    help = "Determine what Python interpreter constraints are used by files/targets."
×
31

UNCOV
32
    summary = BoolOption(
×
33
        default=False,
34
        help=softwrap(
35
            """
36
            Output a CSV summary of interpreter constraints for your whole repository. The
37
            headers are `Target`, `Constraints`, `Transitive Constraints`, `# Dependencies`,
38
            and `# Dependents`.
39

40
            This information can be useful when prioritizing a migration from one Python version to
41
            another (e.g. to Python 3). Use `# Dependencies` and `# Dependents` to help prioritize
42
            which targets are easiest to port (low # dependencies) and highest impact to port
43
            (high # dependents).
44

45
            Use a tool like Pandas or Excel to process the CSV. Use the option
46
            `--py-constraints-output-file=summary.csv` to write directly to a file.
47
            """
48
        ),
49
    )
50

51

UNCOV
52
class PyConstraintsGoal(Goal):
×
UNCOV
53
    subsystem_cls = PyConstraintsSubsystem
×
UNCOV
54
    environment_behavior = Goal.EnvironmentBehavior.LOCAL_ONLY
×
55

56

UNCOV
57
@goal_rule
×
UNCOV
58
async def py_constraints(
×
59
    addresses: Addresses,
60
    console: Console,
61
    py_constraints_subsystem: PyConstraintsSubsystem,
62
    python_setup: PythonSetup,
63
    registered_target_types: RegisteredTargetTypes,
64
    union_membership: UnionMembership,
65
) -> PyConstraintsGoal:
66
    if py_constraints_subsystem.summary:
×
67
        dependents_header = "# Dependents"
×
68
        if addresses:
×
69
            console.print_stderr(
×
70
                softwrap(
71
                    """
72
                    The `py-constraints --summary` goal does not take file/target arguments. Run
73
                    `help py-constraints` for more details.
74
                    """
75
                )
76
            )
77
            return PyConstraintsGoal(exit_code=1)
×
78

79
        all_targets = await find_all_targets()
×
80
        all_python_targets = tuple(
×
81
            t for t in all_targets if t.has_field(InterpreterConstraintsField)
82
        )
83

84
        constraints_per_tgt = [
×
85
            InterpreterConstraints.create_from_targets([tgt], python_setup)
86
            for tgt in all_python_targets
87
        ]
88

89
        transitive_targets_per_tgt = await concurrently(
×
90
            transitive_targets_get(TransitiveTargetsRequest([tgt.address]), **implicitly())
91
            for tgt in all_python_targets
92
        )
93
        transitive_constraints_per_tgt = [
×
94
            InterpreterConstraints.create_from_targets(transitive_targets.closure, python_setup)
95
            for transitive_targets in transitive_targets_per_tgt
96
        ]
97

98
        dependents_per_root = await concurrently(
×
99
            find_dependents(
100
                DependentsRequest([tgt.address], transitive=True, include_roots=False),
101
                **implicitly(),
102
            )
103
            for tgt in all_python_targets
104
        )
105

106
        data = [
×
107
            {
108
                "Target": tgt.address.spec,
109
                "Constraints": str(constraints),
110
                "Transitive Constraints": str(transitive_constraints),
111
                "# Dependencies": len(transitive_targets.dependencies),
112
                dependents_header: len(dependents),
113
            }
114
            for tgt, constraints, transitive_constraints, transitive_targets, dependents in zip(
115
                all_python_targets,
116
                constraints_per_tgt,
117
                transitive_constraints_per_tgt,
118
                transitive_targets_per_tgt,
119
                dependents_per_root,
120
            )
121
        ]
122

123
        with py_constraints_subsystem.output_sink(console) as stdout:
×
124
            writer = csv.DictWriter(
×
125
                stdout,
126
                fieldnames=[
127
                    "Target",
128
                    "Constraints",
129
                    "Transitive Constraints",
130
                    "# Dependencies",
131
                    dependents_header,
132
                ],
133
            )
134
            writer.writeheader()
×
135
            for entry in data:
×
136
                writer.writerow(entry)
×
137

138
        return PyConstraintsGoal(exit_code=0)
×
139

140
    transitive_targets = await transitive_targets_get(
×
141
        TransitiveTargetsRequest(addresses), **implicitly()
142
    )
143
    final_constraints = InterpreterConstraints.create_from_targets(
×
144
        transitive_targets.closure, python_setup
145
    )
146

147
    if not final_constraints:
×
148
        target_types_with_constraints = sorted(
×
149
            tgt_type.alias
150
            for tgt_type in registered_target_types.types
151
            if tgt_type.class_has_field(InterpreterConstraintsField, union_membership)
152
        )
153
        logger.warning(
×
154
            softwrap(
155
                f"""
156
                No Python files/targets matched for the `py-constraints` goal. All target types with
157
                Python interpreter constraints: {", ".join(target_types_with_constraints)}
158
                """
159
            )
160
        )
161
        return PyConstraintsGoal(exit_code=0)
×
162

163
    constraints_to_addresses = defaultdict(set)
×
164
    for tgt in transitive_targets.closure:
×
165
        constraints = InterpreterConstraints.create_from_targets([tgt], python_setup)
×
166
        if not constraints:
×
167
            continue
×
168
        constraints_to_addresses[constraints].add(tgt.address)
×
169

170
    with py_constraints_subsystem.output(console) as output_stdout:
×
171
        output_stdout(f"Final merged constraints: {final_constraints}\n")
×
172
        if len(addresses) > 1:
×
173
            merged_constraints_warning = softwrap(
×
174
                f"""
175
                (These are the constraints used if you were to depend on all of the input
176
                files/targets together, even though they may end up never being used together in
177
                the real world. Consider using a more precise query or running
178
                `{bin_name()} py-constraints --summary`.)\n
179
                """
180
            )
181
            output_stdout(indent(fill(merged_constraints_warning, 80), "  "))
×
182

183
        for constraint, addrs in sorted(constraints_to_addresses.items()):
×
184
            output_stdout(f"\n{constraint}\n")
×
185
            for addr in sorted(addrs):
×
186
                output_stdout(f"  {addr}\n")
×
187

188
    return PyConstraintsGoal(exit_code=0)
×
189

190

UNCOV
191
def rules():
×
UNCOV
192
    return collect_rules()
×
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