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

pantsbuild / pants / 19015773527

02 Nov 2025 05:33PM UTC coverage: 17.872% (-62.4%) from 80.3%
19015773527

Pull #22816

github

web-flow
Merge a12d75757 into 6c024e162
Pull Request #22816: Update Pants internal Python to 3.14

4 of 5 new or added lines in 3 files covered. (80.0%)

28452 existing lines in 683 files now uncovered.

9831 of 55007 relevant lines covered (17.87%)

0.18 hits per line

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

41.82
/src/python/pants/init/specs_calculator.py
1
# Copyright 2018 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
import logging
1✔
5
from typing import cast
1✔
6

7
from pants.base.glob_match_error_behavior import GlobMatchErrorBehavior
1✔
8
from pants.base.specs import AddressLiteralSpec, FileLiteralSpec, RawSpecs, Specs
1✔
9
from pants.base.specs_parser import SpecsParser
1✔
10
from pants.core.environments.rules import determine_bootstrap_environment
1✔
11
from pants.core.util_rules.system_binaries import GitBinary
1✔
12
from pants.engine.addresses import AddressInput
1✔
13
from pants.engine.environment import EnvironmentName
1✔
14
from pants.engine.internals.graph import FilesWithSourceBlocks
1✔
15
from pants.engine.internals.scheduler import SchedulerSession
1✔
16
from pants.engine.internals.selectors import Params
1✔
17
from pants.engine.rules import QueryRule
1✔
18
from pants.option.options import Options
1✔
19
from pants.option.options_bootstrapper import OptionsBootstrapper
1✔
20
from pants.util.frozendict import FrozenDict
1✔
21
from pants.vcs.changed import ChangedAddresses, ChangedOptions, ChangedRequest
1✔
22
from pants.vcs.git import GitWorktreeRequest, MaybeGitWorktree
1✔
23
from pants.vcs.hunk import TextBlocks
1✔
24

25
logger = logging.getLogger(__name__)
1✔
26

27

28
class InvalidSpecConstraint(Exception):
1✔
29
    """Raised when invalid constraints are given via specs and arguments like --changed*."""
30

31

32
def calculate_specs(
1✔
33
    options_bootstrapper: OptionsBootstrapper,
34
    options: Options,
35
    session: SchedulerSession,
36
    working_dir: str,
37
) -> Specs:
38
    """Determine the specs for a given Pants run."""
39
    global_options = options.for_global_scope()
×
40
    unmatched_cli_globs = global_options.unmatched_cli_globs
×
41
    specs = SpecsParser(working_dir=working_dir).parse_specs(
×
42
        options.specs,
43
        description_of_origin="CLI arguments",
44
        unmatched_glob_behavior=unmatched_cli_globs,
45
    )
46

47
    changed_options = ChangedOptions.from_options(options.for_scope("changed"))
×
48
    logger.debug("specs are: %s", specs)
×
49
    logger.debug("changed_options are: %s", changed_options)
×
50

51
    if specs and changed_options.provided:
×
52
        changed_name = "--changed-since" if changed_options.since else "--changed-diffspec"
×
53
        specs_description = specs.arguments_provided_description()
×
54
        assert specs_description is not None
×
55
        raise InvalidSpecConstraint(
×
56
            f"You used `{changed_name}` at the same time as using {specs_description}. You can "
57
            f"only use `{changed_name}` or use normal arguments."
58
        )
59

60
    if not changed_options.provided:
×
61
        return specs
×
62

63
    bootstrap_environment = determine_bootstrap_environment(session)
×
64

65
    (git_binary,) = session.product_request(GitBinary, [Params(bootstrap_environment)])
×
66
    (maybe_git_worktree,) = session.product_request(
×
67
        MaybeGitWorktree, [Params(GitWorktreeRequest(), git_binary, bootstrap_environment)]
68
    )
69
    if not maybe_git_worktree.git_worktree:
×
70
        raise InvalidSpecConstraint(
×
71
            "The `--changed-*` options are only available if Git is used for the repository."
72
        )
73

74
    (files_with_sources_blocks,) = session.product_request(
×
75
        FilesWithSourceBlocks, [Params(bootstrap_environment)]
76
    )
77
    changed_files = tuple(
×
78
        file
79
        for file in changed_options.changed_files(maybe_git_worktree.git_worktree)
80
        # We want to exclude the file from the normal processing flow if it has associated
81
        # targets with text blocks. These files are handled with special logic.
82
        if file not in files_with_sources_blocks
83
    )
84
    file_literal_specs = tuple(FileLiteralSpec(f) for f in changed_files)
×
85

86
    sources_blocks = FrozenDict(
×
87
        (
88
            path,
89
            # Hunk stores information about the old block and the new block.
90
            # Here we only care about the final state, so we take `hunk.right`.
91
            TextBlocks(hunk.right for hunk in hunks if hunk.right is not None),
92
        )
93
        for path, hunks in changed_options.diff_hunks(
94
            maybe_git_worktree.git_worktree,
95
            files_with_sources_blocks,
96
        ).items()
97
    )
98
    logger.debug("changed text blocks: %s", sources_blocks)
×
99

100
    changed_request = ChangedRequest(
×
101
        sources=changed_files,
102
        dependents=changed_options.dependents,
103
        sources_blocks=sources_blocks,
104
    )
105
    (changed_addresses,) = session.product_request(
×
106
        ChangedAddresses,
107
        [Params(changed_request, options_bootstrapper, bootstrap_environment)],
108
    )
109
    logger.debug("changed addresses: %s", changed_addresses)
×
110

111
    address_literal_specs = []
×
112
    for address in cast(ChangedAddresses, changed_addresses):
×
113
        address_input = AddressInput.parse(address.spec, description_of_origin="`--changed-since`")
×
114
        address_literal_specs.append(
×
115
            AddressLiteralSpec(
116
                path_component=address_input.path_component,
117
                target_component=address_input.target_component,
118
                generated_component=address_input.generated_component,
119
                parameters=FrozenDict(address_input.parameters),
120
            )
121
        )
122

123
    return Specs(
×
124
        includes=RawSpecs(
125
            # We need both address_literals and file_literals to cover all our edge cases, including
126
            # target-aware vs. target-less goals, e.g. `list` vs `count-loc`.
127
            address_literals=tuple(address_literal_specs),
128
            file_literals=file_literal_specs,
129
            # The globs here are synthesized from VCS data by the `changed` mechanism.
130
            # As such it does not make sense to apply user-facing matching errors to them.
131
            # In particular, they can legitimately not match anything, if entire git
132
            # subtrees were deleted for example.
133
            unmatched_glob_behavior=GlobMatchErrorBehavior.ignore,
134
            filter_by_global_options=True,
135
            from_change_detection=True,
136
            description_of_origin="`--changed-since`",
137
        ),
138
        ignores=RawSpecs(description_of_origin="`--changed-since`"),
139
    )
140

141

142
def rules():
1✔
UNCOV
143
    return [
×
144
        QueryRule(ChangedAddresses, [ChangedRequest, EnvironmentName]),
145
        QueryRule(GitBinary, [EnvironmentName]),
146
        QueryRule(MaybeGitWorktree, [GitWorktreeRequest, GitBinary, EnvironmentName]),
147
        QueryRule(FilesWithSourceBlocks, [EnvironmentName]),
148
    ]
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