We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
--black-skip breaks fmt goal on isort exception
**Describe the bug**
There are a few related bugs, but the breaking behaviour is the most important I think. I've tested on 2.11, and everything works as expected, so this appears to only be a pre-release/dev bug.

While writing a plugin, I started implementing and testing the `skip` functionality and noticed some strange behaviour on `main`. This behaviour isn't limited to `black`, but it's quick to replicate.

Note below that even after skipping `black` in the `lint` goal, it is in the associated list and marked "succeeded". In reality, it should be failing if it's actually running. In 2.11, black just wouldn't be shown in that list. Personally, I think "black skipped" in yellow text might make more sense, but 🤷🏽 

```bash
> ./pants lint --black-skip ::

✓ autoflake succeeded.
✓ bandit succeeded.
✓ black succeeded.
✕ flake8 failed.
✓ isort succeeded.
✓ pylint succeeded.
✓ pyupgrade succeeded.
✓ shellcheck succeeded.
✓ shfmt succeeded.
```

 The more concerning behaviour is on format.

```bash
> ./pants fmt --black-skip ::

14:28:06.41 [ERROR] 1 Exception encountered:

Engine traceback:
  in select
  in pants.core.goals.fmt.fmt
  in pants.core.goals.fmt.fmt_language
  in pants.backend.python.lint.isort.rules.isort_fmt (isort)
  in pants.engine.process.fallible_to_exec_result_or_raise
Traceback (most recent call last):
  File "/Users/sj/.cache/pants/setup/bootstrap-Darwin-x86_64/pants.NUYNTS/install/lib/python3.9/site-packages/pants/engine/process.py", line 272, in fallible_to_exec_result_or_raise
    raise ProcessExecutionFailure(
pants.engine.process.ProcessExecutionFailure: Process 'Run isort on 7 files.' failed with exit code 1.
```


**Pants version**
2.13.0.dev1

**OS**
MacOS

**Additional info**
Using this repo (https://github.com/sureshjoshi/pants-example-plugin) and I manually set the toml to `2.13.0.dev`.

I also went in and messed with the `main.py` formatting, so I knew `black` would want to run.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/python/pants/core/goals/fmt.py`
Content:
```
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
5 
6 import itertools
7 import logging
8 from collections import defaultdict
9 from dataclasses import dataclass
10 from typing import Iterable, TypeVar
11 
12 from pants.core.goals.style_request import (
13     StyleRequest,
14     determine_specified_tool_names,
15     only_option_help,
16     style_batch_size_help,
17 )
18 from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest
19 from pants.engine.console import Console
20 from pants.engine.engine_aware import EngineAwareReturnType
21 from pants.engine.fs import Digest, MergeDigests, Snapshot, SnapshotDiff, Workspace
22 from pants.engine.goal import Goal, GoalSubsystem
23 from pants.engine.internals.native_engine import EMPTY_SNAPSHOT
24 from pants.engine.process import FallibleProcessResult, ProcessResult
25 from pants.engine.rules import Get, MultiGet, collect_rules, goal_rule, rule
26 from pants.engine.target import FieldSet, FilteredTargets, SourcesField, Targets
27 from pants.engine.unions import UnionMembership, union
28 from pants.option.option_types import IntOption, StrListOption
29 from pants.util.collections import partition_sequentially
30 from pants.util.logging import LogLevel
31 from pants.util.meta import frozen_after_init
32 from pants.util.strutil import strip_v2_chroot_path
33 
34 _F = TypeVar("_F", bound="FmtResult")
35 _FS = TypeVar("_FS", bound=FieldSet)
36 
37 logger = logging.getLogger(__name__)
38 
39 
40 @dataclass(frozen=True)
41 class FmtResult(EngineAwareReturnType):
42     input: Snapshot
43     output: Snapshot
44     stdout: str
45     stderr: str
46     formatter_name: str
47 
48     @classmethod
49     def create(
50         cls,
51         request: FmtRequest,
52         process_result: ProcessResult | FallibleProcessResult,
53         output: Snapshot,
54         *,
55         strip_chroot_path: bool = False,
56     ) -> FmtResult:
57         def prep_output(s: bytes) -> str:
58             return strip_v2_chroot_path(s) if strip_chroot_path else s.decode()
59 
60         return cls(
61             input=request.snapshot,
62             output=output,
63             stdout=prep_output(process_result.stdout),
64             stderr=prep_output(process_result.stderr),
65             formatter_name=request.name,
66         )
67 
68     def __post_init__(self):
69         # NB: We debug log stdout/stderr because `message` doesn't log it.
70         log = f"Output from {self.formatter_name}"
71         if self.stdout:
72             log += f"\n{self.stdout}"
73         if self.stderr:
74             log += f"\n{self.stderr}"
75         logger.debug(log)
76 
77     @classmethod
78     def skip(cls: type[_F], *, formatter_name: str) -> _F:
79         return cls(
80             input=EMPTY_SNAPSHOT,
81             output=EMPTY_SNAPSHOT,
82             stdout="",
83             stderr="",
84             formatter_name=formatter_name,
85         )
86 
87     @property
88     def skipped(self) -> bool:
89         return (
90             self.input == EMPTY_SNAPSHOT
91             and self.output == EMPTY_SNAPSHOT
92             and not self.stdout
93             and not self.stderr
94         )
95 
96     @property
97     def did_change(self) -> bool:
98         return self.output != self.input
99 
100     def level(self) -> LogLevel | None:
101         if self.skipped:
102             return LogLevel.DEBUG
103         return LogLevel.WARN if self.did_change else LogLevel.INFO
104 
105     def message(self) -> str | None:
106         if self.skipped:
107             return f"{self.formatter_name} skipped."
108         message = "made changes." if self.did_change else "made no changes."
109 
110         # NB: Instead of printing out `stdout` and `stderr`, we just print a list of files which
111         # changed. We do this for two reasons:
112         #   1. This is run as part of both `fmt` and `lint`, and we want consistent output between both
113         #   2. Different formatters have different stdout/stderr. This way is consistent across all
114         #       formatters.
115         if self.did_change:
116             output = "".join(
117                 f"\n  {file}"
118                 for file in SnapshotDiff.from_snapshots(self.input, self.output).changed_files
119             )
120         else:
121             output = ""
122 
123         return f"{self.formatter_name} {message}{output}"
124 
125     def cacheable(self) -> bool:
126         """Is marked uncacheable to ensure that it always renders."""
127         return False
128 
129 
130 @union
131 @frozen_after_init
132 @dataclass(unsafe_hash=True)
133 class FmtRequest(StyleRequest[_FS]):
134     snapshot: Snapshot
135 
136     def __init__(self, field_sets: Iterable[_FS], snapshot: Snapshot) -> None:
137         self.snapshot = snapshot
138         super().__init__(field_sets)
139 
140 
141 @dataclass(frozen=True)
142 class _LanguageFmtRequest:
143     request_types: tuple[type[FmtRequest], ...]
144     targets: Targets
145 
146 
147 @dataclass(frozen=True)
148 class _LanguageFmtResults:
149 
150     results: tuple[FmtResult, ...]
151     input: Digest
152     output: Digest
153 
154     @property
155     def did_change(self) -> bool:
156         return self.input != self.output
157 
158 
159 class FmtSubsystem(GoalSubsystem):
160     name = "fmt"
161     help = "Autoformat source code."
162 
163     @classmethod
164     def activated(cls, union_membership: UnionMembership) -> bool:
165         return FmtRequest in union_membership
166 
167     only = StrListOption(
168         "--only",
169         help=only_option_help("fmt", "formatter", "isort", "shfmt"),
170     )
171     batch_size = IntOption(
172         "--batch-size",
173         advanced=True,
174         default=128,
175         help=style_batch_size_help(uppercase="Formatter", lowercase="formatter"),
176     )
177 
178 
179 class Fmt(Goal):
180     subsystem_cls = FmtSubsystem
181 
182 
183 @goal_rule
184 async def fmt(
185     console: Console,
186     targets: FilteredTargets,
187     fmt_subsystem: FmtSubsystem,
188     workspace: Workspace,
189     union_membership: UnionMembership,
190 ) -> Fmt:
191     request_types = union_membership[FmtRequest]
192     specified_names = determine_specified_tool_names("fmt", fmt_subsystem.only, request_types)
193 
194     # Group targets by the sequence of FmtRequests that apply to them.
195     targets_by_fmt_request_order = defaultdict(list)
196     for target in targets:
197         fmt_requests = []
198         for fmt_request in request_types:
199             valid_name = fmt_request.name in specified_names
200             if valid_name and fmt_request.field_set_type.is_applicable(target):  # type: ignore[misc]
201                 fmt_requests.append(fmt_request)
202         if fmt_requests:
203             targets_by_fmt_request_order[tuple(fmt_requests)].append(target)
204 
205     # Spawn sequential formatting per unique sequence of FmtRequests.
206     per_language_results = await MultiGet(
207         Get(
208             _LanguageFmtResults,
209             _LanguageFmtRequest(fmt_requests, Targets(target_batch)),
210         )
211         for fmt_requests, targets in targets_by_fmt_request_order.items()
212         for target_batch in partition_sequentially(
213             targets,
214             key=lambda t: t.address.spec,
215             size_target=fmt_subsystem.batch_size,
216             size_max=4 * fmt_subsystem.batch_size,
217         )
218     )
219 
220     individual_results = list(
221         itertools.chain.from_iterable(
222             language_result.results for language_result in per_language_results
223         )
224     )
225 
226     if not individual_results:
227         return Fmt(exit_code=0)
228 
229     changed_digests = tuple(
230         language_result.output
231         for language_result in per_language_results
232         if language_result.did_change
233     )
234     if changed_digests:
235         # NB: this will fail if there are any conflicting changes, which we want to happen rather
236         # than silently having one result override the other. In practice, this should never
237         # happen due to us grouping each language's formatters into a single digest.
238         merged_formatted_digest = await Get(Digest, MergeDigests(changed_digests))
239         workspace.write_digest(merged_formatted_digest)
240 
241     if individual_results:
242         console.print_stderr("")
243 
244     # We group all results for the same formatter so that we can give one final status in the
245     # summary. This is only relevant if there were multiple results because of
246     # `--per-file-caching`.
247     formatter_to_results = defaultdict(set)
248     for result in individual_results:
249         formatter_to_results[result.formatter_name].add(result)
250 
251     for formatter, results in sorted(formatter_to_results.items()):
252         if any(result.did_change for result in results):
253             sigil = console.sigil_succeeded_with_edits()
254             status = "made changes"
255         elif all(result.skipped for result in results):
256             continue
257         else:
258             sigil = console.sigil_succeeded()
259             status = "made no changes"
260         console.print_stderr(f"{sigil} {formatter} {status}.")
261 
262     # Since the rules to produce FmtResult should use ExecuteRequest, rather than
263     # FallibleProcess, we assume that there were no failures.
264     return Fmt(exit_code=0)
265 
266 
267 @rule
268 async def fmt_language(language_fmt_request: _LanguageFmtRequest) -> _LanguageFmtResults:
269     original_sources = await Get(
270         SourceFiles,
271         SourceFilesRequest(target[SourcesField] for target in language_fmt_request.targets),
272     )
273     prior_formatter_result = original_sources.snapshot
274 
275     results = []
276     for fmt_request_type in language_fmt_request.request_types:
277         request = fmt_request_type(
278             (
279                 fmt_request_type.field_set_type.create(target)
280                 for target in language_fmt_request.targets
281                 if fmt_request_type.field_set_type.is_applicable(target)
282             ),
283             snapshot=prior_formatter_result,
284         )
285         if not request.field_sets:
286             continue
287         result = await Get(FmtResult, FmtRequest, request)
288         results.append(result)
289         prior_formatter_result = result.output
290     return _LanguageFmtResults(
291         tuple(results),
292         input=original_sources.snapshot.digest,
293         output=prior_formatter_result.digest,
294     )
295 
296 
297 def rules():
298     return collect_rules()
299
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/python/pants/core/goals/fmt.py b/src/python/pants/core/goals/fmt.py
--- a/src/python/pants/core/goals/fmt.py
+++ b/src/python/pants/core/goals/fmt.py
@@ -286,7 +286,8 @@
             continue
         result = await Get(FmtResult, FmtRequest, request)
         results.append(result)
-        prior_formatter_result = result.output
+        if not result.skipped:
+            prior_formatter_result = result.output
     return _LanguageFmtResults(
         tuple(results),
         input=original_sources.snapshot.digest,

{"golden_diff": "diff --git a/src/python/pants/core/goals/fmt.py b/src/python/pants/core/goals/fmt.py\n--- a/src/python/pants/core/goals/fmt.py\n+++ b/src/python/pants/core/goals/fmt.py\n@@ -286,7 +286,8 @@\n             continue\n         result = await Get(FmtResult, FmtRequest, request)\n         results.append(result)\n-        prior_formatter_result = result.output\n+        if not result.skipped:\n+            prior_formatter_result = result.output\n     return _LanguageFmtResults(\n         tuple(results),\n         input=original_sources.snapshot.digest,\n", "issue": "--black-skip breaks fmt goal on isort exception\n**Describe the bug**\r\nThere are a few related bugs, but the breaking behaviour is the most important I think. I've tested on 2.11, and everything works as expected, so this appears to only be a pre-release/dev bug.\r\n\r\nWhile writing a plugin, I started implementing and testing the `skip` functionality and noticed some strange behaviour on `main`. This behaviour isn't limited to `black`, but it's quick to replicate.\r\n\r\nNote below that even after skipping `black` in the `lint` goal, it is in the associated list and marked \"succeeded\". In reality, it should be failing if it's actually running. In 2.11, black just wouldn't be shown in that list. Personally, I think \"black skipped\" in yellow text might make more sense, but \ud83e\udd37\ud83c\udffd \r\n\r\n```bash\r\n> ./pants lint --black-skip ::\r\n\r\n\u2713 autoflake succeeded.\r\n\u2713 bandit succeeded.\r\n\u2713 black succeeded.\r\n\u2715 flake8 failed.\r\n\u2713 isort succeeded.\r\n\u2713 pylint succeeded.\r\n\u2713 pyupgrade succeeded.\r\n\u2713 shellcheck succeeded.\r\n\u2713 shfmt succeeded.\r\n```\r\n\r\n The more concerning behaviour is on format.\r\n\r\n```bash\r\n> ./pants fmt --black-skip ::\r\n\r\n14:28:06.41 [ERROR] 1 Exception encountered:\r\n\r\nEngine traceback:\r\n  in select\r\n  in pants.core.goals.fmt.fmt\r\n  in pants.core.goals.fmt.fmt_language\r\n  in pants.backend.python.lint.isort.rules.isort_fmt (isort)\r\n  in pants.engine.process.fallible_to_exec_result_or_raise\r\nTraceback (most recent call last):\r\n  File \"/Users/sj/.cache/pants/setup/bootstrap-Darwin-x86_64/pants.NUYNTS/install/lib/python3.9/site-packages/pants/engine/process.py\", line 272, in fallible_to_exec_result_or_raise\r\n    raise ProcessExecutionFailure(\r\npants.engine.process.ProcessExecutionFailure: Process 'Run isort on 7 files.' failed with exit code 1.\r\n```\r\n\r\n\r\n**Pants version**\r\n2.13.0.dev1\r\n\r\n**OS**\r\nMacOS\r\n\r\n**Additional info**\r\nUsing this repo (https://github.com/sureshjoshi/pants-example-plugin) and I manually set the toml to `2.13.0.dev`.\r\n\r\nI also went in and messed with the `main.py` formatting, so I knew `black` would want to run.\n", "before_files": [{"content": "# Copyright 2019 Pants project contributors (see CONTRIBUTORS.md).\n# Licensed under the Apache License, Version 2.0 (see LICENSE).\n\nfrom __future__ import annotations\n\nimport itertools\nimport logging\nfrom collections import defaultdict\nfrom dataclasses import dataclass\nfrom typing import Iterable, TypeVar\n\nfrom pants.core.goals.style_request import (\n    StyleRequest,\n    determine_specified_tool_names,\n    only_option_help,\n    style_batch_size_help,\n)\nfrom pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest\nfrom pants.engine.console import Console\nfrom pants.engine.engine_aware import EngineAwareReturnType\nfrom pants.engine.fs import Digest, MergeDigests, Snapshot, SnapshotDiff, Workspace\nfrom pants.engine.goal import Goal, GoalSubsystem\nfrom pants.engine.internals.native_engine import EMPTY_SNAPSHOT\nfrom pants.engine.process import FallibleProcessResult, ProcessResult\nfrom pants.engine.rules import Get, MultiGet, collect_rules, goal_rule, rule\nfrom pants.engine.target import FieldSet, FilteredTargets, SourcesField, Targets\nfrom pants.engine.unions import UnionMembership, union\nfrom pants.option.option_types import IntOption, StrListOption\nfrom pants.util.collections import partition_sequentially\nfrom pants.util.logging import LogLevel\nfrom pants.util.meta import frozen_after_init\nfrom pants.util.strutil import strip_v2_chroot_path\n\n_F = TypeVar(\"_F\", bound=\"FmtResult\")\n_FS = TypeVar(\"_FS\", bound=FieldSet)\n\nlogger = logging.getLogger(__name__)\n\n\n@dataclass(frozen=True)\nclass FmtResult(EngineAwareReturnType):\n    input: Snapshot\n    output: Snapshot\n    stdout: str\n    stderr: str\n    formatter_name: str\n\n    @classmethod\n    def create(\n        cls,\n        request: FmtRequest,\n        process_result: ProcessResult | FallibleProcessResult,\n        output: Snapshot,\n        *,\n        strip_chroot_path: bool = False,\n    ) -> FmtResult:\n        def prep_output(s: bytes) -> str:\n            return strip_v2_chroot_path(s) if strip_chroot_path else s.decode()\n\n        return cls(\n            input=request.snapshot,\n            output=output,\n            stdout=prep_output(process_result.stdout),\n            stderr=prep_output(process_result.stderr),\n            formatter_name=request.name,\n        )\n\n    def __post_init__(self):\n        # NB: We debug log stdout/stderr because `message` doesn't log it.\n        log = f\"Output from {self.formatter_name}\"\n        if self.stdout:\n            log += f\"\\n{self.stdout}\"\n        if self.stderr:\n            log += f\"\\n{self.stderr}\"\n        logger.debug(log)\n\n    @classmethod\n    def skip(cls: type[_F], *, formatter_name: str) -> _F:\n        return cls(\n            input=EMPTY_SNAPSHOT,\n            output=EMPTY_SNAPSHOT,\n            stdout=\"\",\n            stderr=\"\",\n            formatter_name=formatter_name,\n        )\n\n    @property\n    def skipped(self) -> bool:\n        return (\n            self.input == EMPTY_SNAPSHOT\n            and self.output == EMPTY_SNAPSHOT\n            and not self.stdout\n            and not self.stderr\n        )\n\n    @property\n    def did_change(self) -> bool:\n        return self.output != self.input\n\n    def level(self) -> LogLevel | None:\n        if self.skipped:\n            return LogLevel.DEBUG\n        return LogLevel.WARN if self.did_change else LogLevel.INFO\n\n    def message(self) -> str | None:\n        if self.skipped:\n            return f\"{self.formatter_name} skipped.\"\n        message = \"made changes.\" if self.did_change else \"made no changes.\"\n\n        # NB: Instead of printing out `stdout` and `stderr`, we just print a list of files which\n        # changed. We do this for two reasons:\n        #   1. This is run as part of both `fmt` and `lint`, and we want consistent output between both\n        #   2. Different formatters have different stdout/stderr. This way is consistent across all\n        #       formatters.\n        if self.did_change:\n            output = \"\".join(\n                f\"\\n  {file}\"\n                for file in SnapshotDiff.from_snapshots(self.input, self.output).changed_files\n            )\n        else:\n            output = \"\"\n\n        return f\"{self.formatter_name} {message}{output}\"\n\n    def cacheable(self) -> bool:\n        \"\"\"Is marked uncacheable to ensure that it always renders.\"\"\"\n        return False\n\n\n@union\n@frozen_after_init\n@dataclass(unsafe_hash=True)\nclass FmtRequest(StyleRequest[_FS]):\n    snapshot: Snapshot\n\n    def __init__(self, field_sets: Iterable[_FS], snapshot: Snapshot) -> None:\n        self.snapshot = snapshot\n        super().__init__(field_sets)\n\n\n@dataclass(frozen=True)\nclass _LanguageFmtRequest:\n    request_types: tuple[type[FmtRequest], ...]\n    targets: Targets\n\n\n@dataclass(frozen=True)\nclass _LanguageFmtResults:\n\n    results: tuple[FmtResult, ...]\n    input: Digest\n    output: Digest\n\n    @property\n    def did_change(self) -> bool:\n        return self.input != self.output\n\n\nclass FmtSubsystem(GoalSubsystem):\n    name = \"fmt\"\n    help = \"Autoformat source code.\"\n\n    @classmethod\n    def activated(cls, union_membership: UnionMembership) -> bool:\n        return FmtRequest in union_membership\n\n    only = StrListOption(\n        \"--only\",\n        help=only_option_help(\"fmt\", \"formatter\", \"isort\", \"shfmt\"),\n    )\n    batch_size = IntOption(\n        \"--batch-size\",\n        advanced=True,\n        default=128,\n        help=style_batch_size_help(uppercase=\"Formatter\", lowercase=\"formatter\"),\n    )\n\n\nclass Fmt(Goal):\n    subsystem_cls = FmtSubsystem\n\n\n@goal_rule\nasync def fmt(\n    console: Console,\n    targets: FilteredTargets,\n    fmt_subsystem: FmtSubsystem,\n    workspace: Workspace,\n    union_membership: UnionMembership,\n) -> Fmt:\n    request_types = union_membership[FmtRequest]\n    specified_names = determine_specified_tool_names(\"fmt\", fmt_subsystem.only, request_types)\n\n    # Group targets by the sequence of FmtRequests that apply to them.\n    targets_by_fmt_request_order = defaultdict(list)\n    for target in targets:\n        fmt_requests = []\n        for fmt_request in request_types:\n            valid_name = fmt_request.name in specified_names\n            if valid_name and fmt_request.field_set_type.is_applicable(target):  # type: ignore[misc]\n                fmt_requests.append(fmt_request)\n        if fmt_requests:\n            targets_by_fmt_request_order[tuple(fmt_requests)].append(target)\n\n    # Spawn sequential formatting per unique sequence of FmtRequests.\n    per_language_results = await MultiGet(\n        Get(\n            _LanguageFmtResults,\n            _LanguageFmtRequest(fmt_requests, Targets(target_batch)),\n        )\n        for fmt_requests, targets in targets_by_fmt_request_order.items()\n        for target_batch in partition_sequentially(\n            targets,\n            key=lambda t: t.address.spec,\n            size_target=fmt_subsystem.batch_size,\n            size_max=4 * fmt_subsystem.batch_size,\n        )\n    )\n\n    individual_results = list(\n        itertools.chain.from_iterable(\n            language_result.results for language_result in per_language_results\n        )\n    )\n\n    if not individual_results:\n        return Fmt(exit_code=0)\n\n    changed_digests = tuple(\n        language_result.output\n        for language_result in per_language_results\n        if language_result.did_change\n    )\n    if changed_digests:\n        # NB: this will fail if there are any conflicting changes, which we want to happen rather\n        # than silently having one result override the other. In practice, this should never\n        # happen due to us grouping each language's formatters into a single digest.\n        merged_formatted_digest = await Get(Digest, MergeDigests(changed_digests))\n        workspace.write_digest(merged_formatted_digest)\n\n    if individual_results:\n        console.print_stderr(\"\")\n\n    # We group all results for the same formatter so that we can give one final status in the\n    # summary. This is only relevant if there were multiple results because of\n    # `--per-file-caching`.\n    formatter_to_results = defaultdict(set)\n    for result in individual_results:\n        formatter_to_results[result.formatter_name].add(result)\n\n    for formatter, results in sorted(formatter_to_results.items()):\n        if any(result.did_change for result in results):\n            sigil = console.sigil_succeeded_with_edits()\n            status = \"made changes\"\n        elif all(result.skipped for result in results):\n            continue\n        else:\n            sigil = console.sigil_succeeded()\n            status = \"made no changes\"\n        console.print_stderr(f\"{sigil} {formatter} {status}.\")\n\n    # Since the rules to produce FmtResult should use ExecuteRequest, rather than\n    # FallibleProcess, we assume that there were no failures.\n    return Fmt(exit_code=0)\n\n\n@rule\nasync def fmt_language(language_fmt_request: _LanguageFmtRequest) -> _LanguageFmtResults:\n    original_sources = await Get(\n        SourceFiles,\n        SourceFilesRequest(target[SourcesField] for target in language_fmt_request.targets),\n    )\n    prior_formatter_result = original_sources.snapshot\n\n    results = []\n    for fmt_request_type in language_fmt_request.request_types:\n        request = fmt_request_type(\n            (\n                fmt_request_type.field_set_type.create(target)\n                for target in language_fmt_request.targets\n                if fmt_request_type.field_set_type.is_applicable(target)\n            ),\n            snapshot=prior_formatter_result,\n        )\n        if not request.field_sets:\n            continue\n        result = await Get(FmtResult, FmtRequest, request)\n        results.append(result)\n        prior_formatter_result = result.output\n    return _LanguageFmtResults(\n        tuple(results),\n        input=original_sources.snapshot.digest,\n        output=prior_formatter_result.digest,\n    )\n\n\ndef rules():\n    return collect_rules()\n", "path": "src/python/pants/core/goals/fmt.py"}], "after_files": [{"content": "# Copyright 2019 Pants project contributors (see CONTRIBUTORS.md).\n# Licensed under the Apache License, Version 2.0 (see LICENSE).\n\nfrom __future__ import annotations\n\nimport itertools\nimport logging\nfrom collections import defaultdict\nfrom dataclasses import dataclass\nfrom typing import Iterable, TypeVar\n\nfrom pants.core.goals.style_request import (\n    StyleRequest,\n    determine_specified_tool_names,\n    only_option_help,\n    style_batch_size_help,\n)\nfrom pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest\nfrom pants.engine.console import Console\nfrom pants.engine.engine_aware import EngineAwareReturnType\nfrom pants.engine.fs import Digest, MergeDigests, Snapshot, SnapshotDiff, Workspace\nfrom pants.engine.goal import Goal, GoalSubsystem\nfrom pants.engine.internals.native_engine import EMPTY_SNAPSHOT\nfrom pants.engine.process import FallibleProcessResult, ProcessResult\nfrom pants.engine.rules import Get, MultiGet, collect_rules, goal_rule, rule\nfrom pants.engine.target import FieldSet, FilteredTargets, SourcesField, Targets\nfrom pants.engine.unions import UnionMembership, union\nfrom pants.option.option_types import IntOption, StrListOption\nfrom pants.util.collections import partition_sequentially\nfrom pants.util.logging import LogLevel\nfrom pants.util.meta import frozen_after_init\nfrom pants.util.strutil import strip_v2_chroot_path\n\n_F = TypeVar(\"_F\", bound=\"FmtResult\")\n_FS = TypeVar(\"_FS\", bound=FieldSet)\n\nlogger = logging.getLogger(__name__)\n\n\n@dataclass(frozen=True)\nclass FmtResult(EngineAwareReturnType):\n    input: Snapshot\n    output: Snapshot\n    stdout: str\n    stderr: str\n    formatter_name: str\n\n    @classmethod\n    def create(\n        cls,\n        request: FmtRequest,\n        process_result: ProcessResult | FallibleProcessResult,\n        output: Snapshot,\n        *,\n        strip_chroot_path: bool = False,\n    ) -> FmtResult:\n        def prep_output(s: bytes) -> str:\n            return strip_v2_chroot_path(s) if strip_chroot_path else s.decode()\n\n        return cls(\n            input=request.snapshot,\n            output=output,\n            stdout=prep_output(process_result.stdout),\n            stderr=prep_output(process_result.stderr),\n            formatter_name=request.name,\n        )\n\n    def __post_init__(self):\n        # NB: We debug log stdout/stderr because `message` doesn't log it.\n        log = f\"Output from {self.formatter_name}\"\n        if self.stdout:\n            log += f\"\\n{self.stdout}\"\n        if self.stderr:\n            log += f\"\\n{self.stderr}\"\n        logger.debug(log)\n\n    @classmethod\n    def skip(cls: type[_F], *, formatter_name: str) -> _F:\n        return cls(\n            input=EMPTY_SNAPSHOT,\n            output=EMPTY_SNAPSHOT,\n            stdout=\"\",\n            stderr=\"\",\n            formatter_name=formatter_name,\n        )\n\n    @property\n    def skipped(self) -> bool:\n        return (\n            self.input == EMPTY_SNAPSHOT\n            and self.output == EMPTY_SNAPSHOT\n            and not self.stdout\n            and not self.stderr\n        )\n\n    @property\n    def did_change(self) -> bool:\n        return self.output != self.input\n\n    def level(self) -> LogLevel | None:\n        if self.skipped:\n            return LogLevel.DEBUG\n        return LogLevel.WARN if self.did_change else LogLevel.INFO\n\n    def message(self) -> str | None:\n        if self.skipped:\n            return f\"{self.formatter_name} skipped.\"\n        message = \"made changes.\" if self.did_change else \"made no changes.\"\n\n        # NB: Instead of printing out `stdout` and `stderr`, we just print a list of files which\n        # changed. We do this for two reasons:\n        #   1. This is run as part of both `fmt` and `lint`, and we want consistent output between both\n        #   2. Different formatters have different stdout/stderr. This way is consistent across all\n        #       formatters.\n        if self.did_change:\n            output = \"\".join(\n                f\"\\n  {file}\"\n                for file in SnapshotDiff.from_snapshots(self.input, self.output).changed_files\n            )\n        else:\n            output = \"\"\n\n        return f\"{self.formatter_name} {message}{output}\"\n\n    def cacheable(self) -> bool:\n        \"\"\"Is marked uncacheable to ensure that it always renders.\"\"\"\n        return False\n\n\n@union\n@frozen_after_init\n@dataclass(unsafe_hash=True)\nclass FmtRequest(StyleRequest[_FS]):\n    snapshot: Snapshot\n\n    def __init__(self, field_sets: Iterable[_FS], snapshot: Snapshot) -> None:\n        self.snapshot = snapshot\n        super().__init__(field_sets)\n\n\n@dataclass(frozen=True)\nclass _LanguageFmtRequest:\n    request_types: tuple[type[FmtRequest], ...]\n    targets: Targets\n\n\n@dataclass(frozen=True)\nclass _LanguageFmtResults:\n\n    results: tuple[FmtResult, ...]\n    input: Digest\n    output: Digest\n\n    @property\n    def did_change(self) -> bool:\n        return self.input != self.output\n\n\nclass FmtSubsystem(GoalSubsystem):\n    name = \"fmt\"\n    help = \"Autoformat source code.\"\n\n    @classmethod\n    def activated(cls, union_membership: UnionMembership) -> bool:\n        return FmtRequest in union_membership\n\n    only = StrListOption(\n        \"--only\",\n        help=only_option_help(\"fmt\", \"formatter\", \"isort\", \"shfmt\"),\n    )\n    batch_size = IntOption(\n        \"--batch-size\",\n        advanced=True,\n        default=128,\n        help=style_batch_size_help(uppercase=\"Formatter\", lowercase=\"formatter\"),\n    )\n\n\nclass Fmt(Goal):\n    subsystem_cls = FmtSubsystem\n\n\n@goal_rule\nasync def fmt(\n    console: Console,\n    targets: FilteredTargets,\n    fmt_subsystem: FmtSubsystem,\n    workspace: Workspace,\n    union_membership: UnionMembership,\n) -> Fmt:\n    request_types = union_membership[FmtRequest]\n    specified_names = determine_specified_tool_names(\"fmt\", fmt_subsystem.only, request_types)\n\n    # Group targets by the sequence of FmtRequests that apply to them.\n    targets_by_fmt_request_order = defaultdict(list)\n    for target in targets:\n        fmt_requests = []\n        for fmt_request in request_types:\n            valid_name = fmt_request.name in specified_names\n            if valid_name and fmt_request.field_set_type.is_applicable(target):  # type: ignore[misc]\n                fmt_requests.append(fmt_request)\n        if fmt_requests:\n            targets_by_fmt_request_order[tuple(fmt_requests)].append(target)\n\n    # Spawn sequential formatting per unique sequence of FmtRequests.\n    per_language_results = await MultiGet(\n        Get(\n            _LanguageFmtResults,\n            _LanguageFmtRequest(fmt_requests, Targets(target_batch)),\n        )\n        for fmt_requests, targets in targets_by_fmt_request_order.items()\n        for target_batch in partition_sequentially(\n            targets,\n            key=lambda t: t.address.spec,\n            size_target=fmt_subsystem.batch_size,\n            size_max=4 * fmt_subsystem.batch_size,\n        )\n    )\n\n    individual_results = list(\n        itertools.chain.from_iterable(\n            language_result.results for language_result in per_language_results\n        )\n    )\n\n    if not individual_results:\n        return Fmt(exit_code=0)\n\n    changed_digests = tuple(\n        language_result.output\n        for language_result in per_language_results\n        if language_result.did_change\n    )\n    if changed_digests:\n        # NB: this will fail if there are any conflicting changes, which we want to happen rather\n        # than silently having one result override the other. In practice, this should never\n        # happen due to us grouping each language's formatters into a single digest.\n        merged_formatted_digest = await Get(Digest, MergeDigests(changed_digests))\n        workspace.write_digest(merged_formatted_digest)\n\n    if individual_results:\n        console.print_stderr(\"\")\n\n    # We group all results for the same formatter so that we can give one final status in the\n    # summary. This is only relevant if there were multiple results because of\n    # `--per-file-caching`.\n    formatter_to_results = defaultdict(set)\n    for result in individual_results:\n        formatter_to_results[result.formatter_name].add(result)\n\n    for formatter, results in sorted(formatter_to_results.items()):\n        if any(result.did_change for result in results):\n            sigil = console.sigil_succeeded_with_edits()\n            status = \"made changes\"\n        elif all(result.skipped for result in results):\n            continue\n        else:\n            sigil = console.sigil_succeeded()\n            status = \"made no changes\"\n        console.print_stderr(f\"{sigil} {formatter} {status}.\")\n\n    # Since the rules to produce FmtResult should use ExecuteRequest, rather than\n    # FallibleProcess, we assume that there were no failures.\n    return Fmt(exit_code=0)\n\n\n@rule\nasync def fmt_language(language_fmt_request: _LanguageFmtRequest) -> _LanguageFmtResults:\n    original_sources = await Get(\n        SourceFiles,\n        SourceFilesRequest(target[SourcesField] for target in language_fmt_request.targets),\n    )\n    prior_formatter_result = original_sources.snapshot\n\n    results = []\n    for fmt_request_type in language_fmt_request.request_types:\n        request = fmt_request_type(\n            (\n                fmt_request_type.field_set_type.create(target)\n                for target in language_fmt_request.targets\n                if fmt_request_type.field_set_type.is_applicable(target)\n            ),\n            snapshot=prior_formatter_result,\n        )\n        if not request.field_sets:\n            continue\n        result = await Get(FmtResult, FmtRequest, request)\n        results.append(result)\n        if not result.skipped:\n            prior_formatter_result = result.output\n    return _LanguageFmtResults(\n        tuple(results),\n        input=original_sources.snapshot.digest,\n        output=prior_formatter_result.digest,\n    )\n\n\ndef rules():\n    return collect_rules()\n", "path": "src/python/pants/core/goals/fmt.py"}]}