pants-pyrefly 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ include *.py
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.4
2
+ Name: pants-pyrefly
3
+ Version: 0.1.0
4
+ Summary: A Pants plugin that runs the Pyrefly type checker in the `check` goal.
5
+ License: Apache-2.0
6
+ Keywords: pants,pantsbuild,pants-plugin,pyrefly,typecheck
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: License :: OSI Approved :: Apache Software License
9
+ Classifier: Operating System :: MacOS :: MacOS X
10
+ Classifier: Operating System :: POSIX :: Linux
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Topic :: Software Development :: Build Tools
14
+ Requires-Python: >=3.12
15
+ Description-Content-Type: text/markdown
16
+ Dynamic: classifier
17
+ Dynamic: description
18
+ Dynamic: description-content-type
19
+ Dynamic: keywords
20
+ Dynamic: license
21
+ Dynamic: requires-python
22
+ Dynamic: summary
23
+
24
+ # pants-pyrefly
25
+
26
+ A [Pants](https://www.pantsbuild.org/) plugin that runs [Pyrefly](https://pyrefly.org/) —
27
+ Meta's fast, Rust-based Python type checker — as part of the Pants `check` goal.
28
+
29
+ Pants downloads the official prebuilt Pyrefly binary (pinned by SHA256) and runs it hermetically
30
+ in a sandbox, wiring up your first-party source roots and the resolved third-party dependencies so
31
+ that imports resolve correctly.
32
+
33
+ ## Requirements
34
+
35
+ - **Pants 2.27–2.32.** A single codebase supports both the legacy (`Get`/`MultiGet`-era) and modern
36
+ (call-by-name) rules APIs via a small version-conditional import; verified on 2.27 and 2.32.
37
+ - The **published wheel** is pure-Python — `Requires-Python: >=3.12`, with **no `pantsbuild.pants`
38
+ dependency** (Pants provides itself at runtime) — so it installs into any Pants whose runtime is
39
+ CPython 3.12+ (e.g. 2.32 → CPython 3.14). **Pants 2.27** runs on CPython 3.11, below the floor —
40
+ install **from source** there (see [Installation](#installation)); the plugin code still supports it.
41
+
42
+ ## Installation
43
+
44
+ Add the plugin and enable its backend in `pants.toml`:
45
+
46
+ ```toml
47
+ [GLOBAL]
48
+ plugins = ["pants-pyrefly==0.1.0"]
49
+ backend_packages.add = [
50
+ "pants.backend.python",
51
+ "pants_pyrefly",
52
+ ]
53
+ ```
54
+
55
+ ### From source (in-repo)
56
+
57
+ Prefer to vendor the plugin — for rapid iteration, to pin to an exact source state, or to run on
58
+ **Pants 2.27** (its CPython 3.11 runtime is below the published wheel's floor)? Consume it the way
59
+ in-repo plugins are normally loaded: copy `pants-plugins/pants_pyrefly/` into your repo and:
60
+
61
+ ```toml
62
+ [GLOBAL]
63
+ pythonpath = ["%(buildroot)s/pants-plugins"]
64
+ backend_packages.add = ["pants.backend.python", "pants_pyrefly"]
65
+ ```
66
+
67
+ If you keep plugin code in a dedicated `pants-plugins` resolve, add it there and run
68
+ `pants generate-lockfiles`.
69
+
70
+ ## Usage
71
+
72
+ ```bash
73
+ pants check :: # type-check everything
74
+ pants check path/to/dir:: # type-check a subtree
75
+ ```
76
+
77
+ ## Configuration
78
+
79
+ `[pyrefly]` subsystem options:
80
+
81
+ | Option | Env / flag | Description |
82
+ | --- | --- | --- |
83
+ | `skip` | `--pyrefly-skip` / `PANTS_PYREFLY_SKIP` | Don't run Pyrefly during `check`. |
84
+ | `args` | `--pyrefly-args` | Extra args passed to Pyrefly, e.g. `--pyrefly-args='--python-version 3.12'`. |
85
+ | `extra_type_stubs` | `--pyrefly-extra-type-stubs` | Stub-only packages to add to the type-check environment without making them runtime deps, e.g. `types-requests`, `sqlalchemy2-stubs==0.0.2a38`. Resolved directly, so pin versions for reproducibility. |
86
+ | `output_format` | `--pyrefly-output-format` | Override Pyrefly's output format: `min-text`, `full-text`, `json`, `github`, `junit-xml`, `omit-errors`. |
87
+ | `min_severity` | `--pyrefly-min-severity` | Only show errors at/above this severity (`ignore`/`info`/`warn`/`error`). |
88
+ | `only` | `--pyrefly-only` | Only report these error kinds (e.g. `bad-assignment`); handy for triage. |
89
+ | `config` | `--pyrefly-config` | Path to a `pyrefly.toml` / `pyproject.toml` (disables discovery). |
90
+ | `config_discovery` | `--[no-]pyrefly-config-discovery` | Auto-discover `pyrefly.toml` / `[tool.pyrefly]`. |
91
+ | `baseline` | `--pyrefly-baseline` | Path to a Pyrefly baseline JSON; `check` then reports only errors *new* since the baseline. Generate it with `pants pyrefly-update-baseline`. |
92
+ | `version` / `known_versions` / `url_template` | (advanced) | Pin or override the downloaded Pyrefly binary. |
93
+
94
+ Opt a target out of Pyrefly:
95
+
96
+ ```python
97
+ python_sources(skip_pyrefly=True)
98
+ ```
99
+
100
+ ## Incremental adoption (baseline)
101
+
102
+ Adopting Pyrefly on a codebase that already has type errors? Record them in a baseline so `check`
103
+ only fails on *new* errors:
104
+
105
+ ```bash
106
+ pants pyrefly-update-baseline :: # writes the file named by [pyrefly].baseline
107
+ pants check :: # now reports only errors introduced since the baseline
108
+ ```
109
+
110
+ Configure the path (and commit the baseline file):
111
+
112
+ ```toml
113
+ [pyrefly]
114
+ baseline = "build-support/pyrefly-baseline.json"
115
+ ```
116
+
117
+ Re-run `pants pyrefly-update-baseline` after fixing errors, or to refresh it. Baseline matching is
118
+ Pyrefly's own (lenient by design, so it survives code churn).
119
+
120
+ ## Editor / IDE (LSP)
121
+
122
+ Pyrefly ships an LSP server, but in a Pants repo your editor doesn't know the source roots. Generate
123
+ a `pyrefly.toml` with them:
124
+
125
+ ```bash
126
+ pants pyrefly-lsp-config # writes search-path (= your source roots) + python-version
127
+ ```
128
+
129
+ For third-party imports, point your editor's interpreter at a venv (e.g.
130
+ `pants export --resolve=python-default`). If your Pyrefly config lives in `pyproject.toml`
131
+ `[tool.pyrefly]`, the goal prints the keys to add instead of writing a shadowing `pyrefly.toml`.
132
+
133
+ ## Type coverage
134
+
135
+ Track typing progress — useful as a migration ratchet:
136
+
137
+ ```bash
138
+ pants pyrefly-coverage :: # prints overall % typed
139
+ pants pyrefly-coverage --pyrefly-coverage-fail-under=80 :: # also fails if below 80%
140
+ ```
141
+
142
+ ## How import resolution works
143
+
144
+ - **First-party code:** every source root is passed to Pyrefly via `--search-path` (the analogue of
145
+ `MYPYPATH` / `sys.path`).
146
+ - **Third-party deps:** Pants materializes the target's resolved requirements into a venv and points
147
+ Pyrefly's `--python-interpreter-path` at it, so Pyrefly discovers `site-packages` and the target
148
+ Python version exactly as `import` would at runtime.
149
+
150
+ ## Pants compatibility
151
+
152
+ | Plugin version | Pants | Pyrefly (default) |
153
+ | --- | --- | --- |
154
+ | `0.1.0` | `2.27`–`2.32` | `1.1.1` |
155
+
156
+ The plugin supports both the legacy (`Get`/`MultiGet`) and modern (call-by-name) rules APIs through
157
+ a small version-conditional import (the rules API changed at Pants 2.30, and again removed `Get`
158
+ by 2.32). Verified on 2.27 and 2.32; in-between versions use the same modern API.
159
+
160
+ ## Development
161
+
162
+ This repo dogfoods its own tooling: `ruff` (lint + format) and Pyrefly itself (`check`) run on the
163
+ plugin's sources.
164
+
165
+ ```bash
166
+ pants generate-lockfiles # pants-plugins + python-default resolves
167
+ pants fmt lint :: # ruff format + check
168
+ pants check :: # Pyrefly type-checks the plugin (dogfood) + testprojects/
169
+ pants test :: # run the integration tests
170
+ pants package pants-plugins/pants_pyrefly:dist # build the wheel + sdist into dist/
171
+ ```
172
+
173
+ ## Releasing
174
+
175
+ Push a `vX.Y.Z` tag. The [release workflow](.github/workflows/release.yml) builds the wheel and
176
+ publishes it to PyPI using [Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC,
177
+ no API tokens). Configure a PyPI trusted publisher for this repo + the `release.yml` workflow first.
178
+
179
+ ## License
180
+
181
+ [Apache-2.0](LICENSE).
@@ -0,0 +1,31 @@
1
+
2
+ # DO NOT EDIT THIS FILE -- AUTOGENERATED BY PANTS
3
+
4
+ import errno
5
+ import os
6
+ import setuptools.build_meta
7
+
8
+ backend = setuptools.build_meta.__legacy__
9
+
10
+ dist_dir = "dist/"
11
+ build_wheel = True
12
+ build_sdist = True
13
+ wheel_config_settings = {
14
+ }
15
+ sdist_config_settings = {
16
+ }
17
+
18
+ # Python 2.7 doesn't have the exist_ok arg on os.makedirs().
19
+ try:
20
+ os.makedirs(dist_dir)
21
+ except OSError as e:
22
+ if e.errno != errno.EEXIST:
23
+ raise
24
+
25
+ wheel_path = backend.build_wheel(dist_dir, wheel_config_settings) if build_wheel else None
26
+ sdist_path = backend.build_sdist(dist_dir, sdist_config_settings) if build_sdist else None
27
+
28
+ if wheel_path:
29
+ print("wheel: {wheel_path}".format(wheel_path=wheel_path))
30
+ if sdist_path:
31
+ print("sdist: {sdist_path}".format(sdist_path=sdist_path))
File without changes
@@ -0,0 +1,320 @@
1
+ # Copyright 2026 Tague Griffith
2
+ # Licensed under the Apache License, Version 2.0 (see LICENSE).
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import logging
8
+ from collections.abc import Iterable
9
+
10
+ import toml # pants: no-infer-dep (provided by the Pants runtime)
11
+
12
+ from pants_pyrefly.rules import (
13
+ _BASELINE_OUTPUT,
14
+ PyreflyFieldSet,
15
+ PyreflyRequest,
16
+ _setup_pyrefly_process,
17
+ pyrefly_determine_partitions,
18
+ )
19
+ from pants_pyrefly.subsystems import Pyrefly
20
+
21
+ from pants.backend.python.subsystems.setup import PythonSetup
22
+ from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
23
+ from pants.backend.python.util_rules.python_sources import (
24
+ PythonSourceFilesRequest,
25
+ prepare_python_sources,
26
+ )
27
+ from pants.engine.fs import (
28
+ EMPTY_DIGEST,
29
+ CreateDigest,
30
+ FileContent,
31
+ GlobMatchErrorBehavior,
32
+ PathGlobs,
33
+ Workspace,
34
+ )
35
+ from pants.engine.console import Console
36
+ from pants.engine.goal import Goal, GoalSubsystem
37
+ from pants.engine.intrinsics import (
38
+ create_digest,
39
+ execute_process,
40
+ get_digest_contents,
41
+ path_globs_to_digest,
42
+ )
43
+ from pants.engine.platform import Platform
44
+ from pants.engine.process import ProcessCacheScope
45
+ from pants.engine.rules import Rule, collect_rules, concurrently, goal_rule, implicitly
46
+ from pants.engine.target import AllTargets, Targets
47
+ from pants.engine.unions import UnionRule
48
+ from pants.option.option_types import FloatOption
49
+ from pants.util.strutil import softwrap
50
+
51
+ logger = logging.getLogger(__name__)
52
+
53
+
54
+ # ---
55
+ # `pyrefly-update-baseline`
56
+ # ---
57
+
58
+
59
+ class PyreflyUpdateBaselineSubsystem(GoalSubsystem):
60
+ name = "pyrefly-update-baseline"
61
+ help = softwrap(
62
+ """
63
+ Run Pyrefly and (re)write the baseline file configured by `[pyrefly].baseline`, recording
64
+ the current type errors so that a subsequent `pants check` reports only NEW ones.
65
+ """
66
+ )
67
+
68
+
69
+ class PyreflyUpdateBaseline(Goal):
70
+ subsystem_cls = PyreflyUpdateBaselineSubsystem
71
+ environment_behavior = Goal.EnvironmentBehavior.LOCAL_ONLY
72
+
73
+
74
+ @goal_rule
75
+ async def pyrefly_update_baseline(
76
+ targets: Targets,
77
+ pyrefly: Pyrefly,
78
+ workspace: Workspace,
79
+ platform: Platform,
80
+ python_setup: PythonSetup,
81
+ ) -> PyreflyUpdateBaseline:
82
+ if not pyrefly.baseline:
83
+ logger.error(
84
+ softwrap(
85
+ """
86
+ Set `[pyrefly].baseline` to the path where the baseline file should be written,
87
+ then re-run `pants pyrefly-update-baseline`.
88
+ """
89
+ )
90
+ )
91
+ return PyreflyUpdateBaseline(exit_code=1)
92
+
93
+ field_sets = tuple(
94
+ PyreflyFieldSet.create(tgt)
95
+ for tgt in targets
96
+ if PyreflyFieldSet.is_applicable(tgt) and not PyreflyFieldSet.opt_out(tgt)
97
+ )
98
+ if not field_sets:
99
+ logger.warning("No Pyrefly-applicable targets in scope; the baseline was not changed.")
100
+ return PyreflyUpdateBaseline(exit_code=0)
101
+
102
+ partitions = await pyrefly_determine_partitions(PyreflyRequest(field_sets), **implicitly())
103
+ processes = await concurrently(
104
+ _setup_pyrefly_process(
105
+ partition,
106
+ pyrefly,
107
+ platform,
108
+ python_setup,
109
+ update_baseline=True,
110
+ cache_scope=ProcessCacheScope.PER_SESSION,
111
+ )
112
+ for partition in partitions
113
+ )
114
+ results = await concurrently(execute_process(process, **implicitly()) for process in processes)
115
+
116
+ for result in results:
117
+ # 0 == no errors, 1 == errors found (both expected); anything else is a tool failure, and
118
+ # we must not clobber a good baseline with a partial/empty one.
119
+ if result.exit_code not in (0, 1):
120
+ logger.error(
121
+ f"Pyrefly exited with code {result.exit_code} while updating the baseline; "
122
+ f"leaving the existing baseline unchanged.\n"
123
+ f"{result.stderr.decode(errors='replace')}"
124
+ )
125
+ return PyreflyUpdateBaseline(exit_code=result.exit_code)
126
+
127
+ # Merge the per-partition baselines (each is `{"errors": [...]}`) into a single file.
128
+ all_errors: list = []
129
+ for result in results:
130
+ for file_content in await get_digest_contents(result.output_digest):
131
+ if file_content.path == _BASELINE_OUTPUT and file_content.content.strip():
132
+ all_errors.extend(json.loads(file_content.content).get("errors", []))
133
+
134
+ merged = json.dumps({"errors": all_errors}, indent=2).encode()
135
+ output_digest = await create_digest(CreateDigest([FileContent(pyrefly.baseline, merged)]))
136
+ workspace.write_digest(output_digest)
137
+ logger.info(f"Wrote Pyrefly baseline with {len(all_errors)} error(s) to `{pyrefly.baseline}`.")
138
+ return PyreflyUpdateBaseline(exit_code=0)
139
+
140
+
141
+ # ---
142
+ # `pyrefly-lsp-config`
143
+ # ---
144
+
145
+
146
+ class PyreflyLspConfigSubsystem(GoalSubsystem):
147
+ name = "pyrefly-lsp-config"
148
+ help = softwrap(
149
+ """
150
+ Write Pants's source roots (and target Python version) into `pyrefly.toml` as `search-path`,
151
+ so the Pyrefly IDE/LSP resolves first-party imports the way Pants does. Point your editor's
152
+ interpreter at a venv (e.g. `pants export`) for third-party imports.
153
+ """
154
+ )
155
+
156
+
157
+ class PyreflyLspConfig(Goal):
158
+ subsystem_cls = PyreflyLspConfigSubsystem
159
+ environment_behavior = Goal.EnvironmentBehavior.LOCAL_ONLY
160
+
161
+
162
+ @goal_rule
163
+ async def pyrefly_lsp_config(
164
+ all_targets: AllTargets,
165
+ python_setup: PythonSetup,
166
+ workspace: Workspace,
167
+ ) -> PyreflyLspConfig:
168
+ python_targets = [tgt for tgt in all_targets if PyreflyFieldSet.is_applicable(tgt)]
169
+ if not python_targets:
170
+ logger.warning("No Python source targets found; not writing a Pyrefly LSP config.")
171
+ return PyreflyLspConfig(exit_code=0)
172
+
173
+ sources = await prepare_python_sources(PythonSourceFilesRequest(python_targets), **implicitly())
174
+ python_version = InterpreterConstraints(
175
+ python_setup.interpreter_constraints
176
+ ).minimum_python_version(python_setup.interpreter_versions_universe)
177
+
178
+ settings: dict = {"search-path": sorted(sources.source_roots)}
179
+ if python_version:
180
+ settings["python-version"] = python_version
181
+
182
+ pyrefly_toml_digest, pyproject_digest = await concurrently(
183
+ path_globs_to_digest(
184
+ PathGlobs(["pyrefly.toml"], glob_match_error_behavior=GlobMatchErrorBehavior.ignore)
185
+ ),
186
+ path_globs_to_digest(
187
+ PathGlobs(["pyproject.toml"], glob_match_error_behavior=GlobMatchErrorBehavior.ignore)
188
+ ),
189
+ )
190
+
191
+ pyproject_has_pyrefly = False
192
+ if pyproject_digest != EMPTY_DIGEST:
193
+ pyproject_text = (await get_digest_contents(pyproject_digest))[0].content.decode()
194
+ pyproject_has_pyrefly = "pyrefly" in toml.loads(pyproject_text).get("tool", {})
195
+
196
+ # Don't create a `pyrefly.toml` that would shadow an existing `pyproject.toml [tool.pyrefly]`.
197
+ if pyrefly_toml_digest == EMPTY_DIGEST and pyproject_has_pyrefly:
198
+ logger.warning(
199
+ softwrap(
200
+ """
201
+ Your Pyrefly config lives in `pyproject.toml` under `[tool.pyrefly]`. A separate
202
+ `pyrefly.toml` would take precedence over it, so it was not written. Add these keys
203
+ to your `[tool.pyrefly]` table instead:
204
+ """
205
+ )
206
+ + f"\n\n{toml.dumps(settings).strip()}\n"
207
+ )
208
+ return PyreflyLspConfig(exit_code=0)
209
+
210
+ # Merge into an existing `pyrefly.toml` (preserving other keys) or create a new one.
211
+ existing: dict = {}
212
+ if pyrefly_toml_digest != EMPTY_DIGEST:
213
+ existing = toml.loads((await get_digest_contents(pyrefly_toml_digest))[0].content.decode())
214
+ existing.update(settings)
215
+
216
+ output_digest = await create_digest(
217
+ CreateDigest([FileContent("pyrefly.toml", toml.dumps(existing).encode())])
218
+ )
219
+ workspace.write_digest(output_digest)
220
+ logger.info(
221
+ f"Wrote {len(settings['search-path'])} source root(s) to `pyrefly.toml` as Pyrefly "
222
+ f"`search-path`. Point your editor's interpreter at a venv (e.g. `pants export`) for "
223
+ f"third-party imports."
224
+ )
225
+ return PyreflyLspConfig(exit_code=0)
226
+
227
+
228
+ # ---
229
+ # `pyrefly-coverage`
230
+ # ---
231
+
232
+
233
+ class PyreflyCoverageSubsystem(GoalSubsystem):
234
+ name = "pyrefly-coverage"
235
+ help = softwrap(
236
+ """
237
+ Report Pyrefly type coverage — the share of typable symbols that have a non-`Any` type —
238
+ across the targeted Python sources.
239
+ """
240
+ )
241
+
242
+ fail_under = FloatOption(
243
+ default=None,
244
+ help="If set, exit non-zero when overall type coverage is below this percentage (0-100).",
245
+ )
246
+
247
+
248
+ class PyreflyCoverage(Goal):
249
+ subsystem_cls = PyreflyCoverageSubsystem
250
+ environment_behavior = Goal.EnvironmentBehavior.LOCAL_ONLY
251
+
252
+
253
+ @goal_rule
254
+ async def pyrefly_coverage(
255
+ targets: Targets,
256
+ pyrefly: Pyrefly,
257
+ coverage_subsystem: PyreflyCoverageSubsystem,
258
+ console: Console,
259
+ platform: Platform,
260
+ python_setup: PythonSetup,
261
+ ) -> PyreflyCoverage:
262
+ field_sets = tuple(
263
+ PyreflyFieldSet.create(tgt)
264
+ for tgt in targets
265
+ if PyreflyFieldSet.is_applicable(tgt) and not PyreflyFieldSet.opt_out(tgt)
266
+ )
267
+ if not field_sets:
268
+ logger.warning("No Pyrefly-applicable targets in scope.")
269
+ return PyreflyCoverage(exit_code=0)
270
+
271
+ partitions = await pyrefly_determine_partitions(PyreflyRequest(field_sets), **implicitly())
272
+ processes = await concurrently(
273
+ _setup_pyrefly_process(
274
+ partition,
275
+ pyrefly,
276
+ platform,
277
+ python_setup,
278
+ subcommand=("coverage", "report"),
279
+ cache_scope=ProcessCacheScope.SUCCESSFUL,
280
+ )
281
+ for partition in partitions
282
+ )
283
+ results = await concurrently(execute_process(process, **implicitly()) for process in processes)
284
+
285
+ total_typable = 0
286
+ total_typed = 0
287
+ for result in results:
288
+ if result.exit_code != 0:
289
+ logger.error(
290
+ f"Pyrefly coverage failed (exit {result.exit_code}):\n"
291
+ f"{result.stderr.decode(errors='replace')}"
292
+ )
293
+ return PyreflyCoverage(exit_code=result.exit_code)
294
+ try:
295
+ report = json.loads(result.stdout)
296
+ except json.JSONDecodeError:
297
+ logger.error(
298
+ "Could not parse Pyrefly coverage output as JSON:\n"
299
+ f"{result.stdout.decode(errors='replace')[:500]}"
300
+ )
301
+ return PyreflyCoverage(exit_code=1)
302
+ for module_report in report.get("module_reports", []):
303
+ for symbol in module_report.get("symbol_reports", []):
304
+ total_typable += symbol.get("n_typable", 0)
305
+ total_typed += symbol.get("n_typed", 0)
306
+
307
+ pct = (100.0 * total_typed / total_typable) if total_typable else 100.0
308
+ console.print_stdout(
309
+ f"Pyrefly type coverage: {pct:.1f}% ({total_typed}/{total_typable} typable symbols)"
310
+ )
311
+ if coverage_subsystem.fail_under is not None and pct < coverage_subsystem.fail_under:
312
+ console.print_stderr(
313
+ f"Type coverage {pct:.1f}% is below the required {coverage_subsystem.fail_under}%."
314
+ )
315
+ return PyreflyCoverage(exit_code=1)
316
+ return PyreflyCoverage(exit_code=0)
317
+
318
+
319
+ def rules() -> Iterable[Rule | UnionRule]:
320
+ return (*collect_rules(),)
@@ -0,0 +1,23 @@
1
+ # Copyright 2026 Tague Griffith
2
+ # Licensed under the Apache License, Version 2.0 (see LICENSE).
3
+
4
+ """A Pants plugin that runs the Pyrefly type checker (https://pyrefly.org) in the `check` goal."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Iterable
9
+
10
+ from pants_pyrefly import goals
11
+ from pants_pyrefly import rules as pyrefly_rules
12
+ from pants_pyrefly import skip_field, subsystems
13
+ from pants.engine.rules import Rule
14
+ from pants.engine.unions import UnionRule
15
+
16
+
17
+ def rules() -> Iterable[Rule | UnionRule]:
18
+ return (
19
+ *pyrefly_rules.rules(),
20
+ *goals.rules(),
21
+ *skip_field.rules(),
22
+ *subsystems.rules(),
23
+ )