pants-ty 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.
pants_ty-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vasile Razdalovschi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: pants-ty
3
+ Version: 0.1.0
4
+ Summary: A Pants check backend for the Astral ty Python type checker.
5
+ Author: Vasile Razdalovschi
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/vrazdalovschi/pants-ty-plugin
8
+ Project-URL: Issues, https://github.com/vrazdalovschi/pants-ty-plugin/issues
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Topic :: Software Development :: Build Tools
14
+ Requires-Python: >=3.11
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: pantsbuild.pants<2.32,>=2.31
18
+ Dynamic: license-file
19
+
20
+ # pants-ty-plugin
21
+
22
+ `pants-ty-plugin` adds a Pants `check` backend for [Astral ty](https://docs.astral.sh/ty/).
23
+
24
+ It is designed for Pants `2.31.x` and currently supports Linux and macOS. The backend installs
25
+ `ty` as an external binary, then runs it with:
26
+
27
+ - a resolve-backed Python environment for third-party dependencies
28
+ - Pants source roots as `--extra-search-path` entries for first-party imports
29
+
30
+ That means `ty` can resolve imports that Pants already knows about.
31
+
32
+ ## Features
33
+
34
+ - `pants check --only=ty ...`
35
+ - `skip_ty = true` on Python targets
36
+ - `ty.toml` discovery
37
+ - `[tool.ty]` discovery from `pyproject.toml`
38
+ - resolve-aware `--python`
39
+ - source-root-aware `--extra-search-path`
40
+
41
+ ## Install
42
+
43
+ ### Option 1: vendor the plugin into a private repo
44
+
45
+ Copy only the Python package files from `pants-plugins/pants_ty/` into your repo's
46
+ `pants-plugins/pants_ty/` directory:
47
+
48
+ - `__init__.py`
49
+ - `register.py`
50
+ - `rules.py`
51
+ - `skip_field.py`
52
+ - `subsystem.py`
53
+
54
+ Do not copy this plugin repo's development-only files:
55
+
56
+ - `pants-plugins/BUILD`
57
+ - `pants-plugins/pants_ty/BUILD`
58
+ - `pants-plugins/lock.txt`
59
+ - `tests/`
60
+ - this repo's `pants.toml`, `pyproject.toml`, or GitHub workflow files
61
+
62
+ Those files are only for developing and releasing `pants-ty-plugin` itself. A consuming repo
63
+ should use its own resolves, lockfiles, and test setup.
64
+
65
+ Then configure:
66
+
67
+ ```toml
68
+ [GLOBAL]
69
+ pants_version = "2.31.0"
70
+ backend_packages = [
71
+ "pants.backend.python",
72
+ "pants_ty",
73
+ ]
74
+ pythonpath = ["%(buildroot)s/pants-plugins"]
75
+ ```
76
+
77
+ If you accidentally copy the dev `BUILD` files too, you may see an error like:
78
+
79
+ ```text
80
+ UnrecognizedResolveNamesError: ... resolve ... pants-plugins
81
+ ```
82
+
83
+ That means your consuming repo picked up this repo's internal development resolve. Remove the
84
+ copied `BUILD` files and keep only the plugin Python modules.
85
+
86
+ ### Option 2: install as a published plugin
87
+
88
+ Once the package is published, use:
89
+
90
+ ```toml
91
+ [GLOBAL]
92
+ plugins = ["pants-ty==0.1.0"]
93
+ backend_packages = [
94
+ "pants.backend.python",
95
+ "pants_ty",
96
+ ]
97
+ ```
98
+
99
+ ## Configure
100
+
101
+ Create either `ty.toml` or add `[tool.ty]` to `pyproject.toml`.
102
+
103
+ Example:
104
+
105
+ ```toml
106
+ [tool.ty]
107
+ exclude = [".pants.d", "dist"]
108
+ ```
109
+
110
+ Pants exposes the plugin options under `[ty]`:
111
+
112
+ ```toml
113
+ [ty]
114
+ args = ["--output-format=concise"]
115
+ config_discovery = true
116
+ ```
117
+
118
+ Useful commands:
119
+
120
+ ```bash
121
+ pants help ty
122
+ pants check --only=ty ::
123
+ pants check --only=ty path/to/pkg::
124
+ ```
125
+
126
+ To skip a target:
127
+
128
+ ```python
129
+ python_sources(
130
+ name="lib",
131
+ skip_ty=True,
132
+ )
133
+ ```
134
+
135
+ ## Development
136
+
137
+ This repo uses Pants to lint, test, and dogfood the plugin itself.
138
+
139
+ ```bash
140
+ ruff check pants-plugins tests
141
+ pants test ::
142
+ pants check ::
143
+ ```
144
+
145
+ To build a distributable wheel and sdist:
146
+
147
+ ```bash
148
+ python -m build
149
+ ```
150
+
151
+ ## Repository layout
152
+
153
+ - `pants-plugins/pants_ty`: plugin source
154
+ - `tests/pants_ty`: unit and integration tests
155
+ - `pants.toml`: local development config
156
+ - `pants.ci.toml`: CI-specific Pants settings
157
+
158
+ ## Notes
159
+
160
+ - The Pants plugin API is not stable across minor versions. This repo currently targets
161
+ Pants `2.31.x`.
162
+ - The backend only resolves imports that Pants already knows through source roots and
163
+ target resolves.
@@ -0,0 +1,144 @@
1
+ # pants-ty-plugin
2
+
3
+ `pants-ty-plugin` adds a Pants `check` backend for [Astral ty](https://docs.astral.sh/ty/).
4
+
5
+ It is designed for Pants `2.31.x` and currently supports Linux and macOS. The backend installs
6
+ `ty` as an external binary, then runs it with:
7
+
8
+ - a resolve-backed Python environment for third-party dependencies
9
+ - Pants source roots as `--extra-search-path` entries for first-party imports
10
+
11
+ That means `ty` can resolve imports that Pants already knows about.
12
+
13
+ ## Features
14
+
15
+ - `pants check --only=ty ...`
16
+ - `skip_ty = true` on Python targets
17
+ - `ty.toml` discovery
18
+ - `[tool.ty]` discovery from `pyproject.toml`
19
+ - resolve-aware `--python`
20
+ - source-root-aware `--extra-search-path`
21
+
22
+ ## Install
23
+
24
+ ### Option 1: vendor the plugin into a private repo
25
+
26
+ Copy only the Python package files from `pants-plugins/pants_ty/` into your repo's
27
+ `pants-plugins/pants_ty/` directory:
28
+
29
+ - `__init__.py`
30
+ - `register.py`
31
+ - `rules.py`
32
+ - `skip_field.py`
33
+ - `subsystem.py`
34
+
35
+ Do not copy this plugin repo's development-only files:
36
+
37
+ - `pants-plugins/BUILD`
38
+ - `pants-plugins/pants_ty/BUILD`
39
+ - `pants-plugins/lock.txt`
40
+ - `tests/`
41
+ - this repo's `pants.toml`, `pyproject.toml`, or GitHub workflow files
42
+
43
+ Those files are only for developing and releasing `pants-ty-plugin` itself. A consuming repo
44
+ should use its own resolves, lockfiles, and test setup.
45
+
46
+ Then configure:
47
+
48
+ ```toml
49
+ [GLOBAL]
50
+ pants_version = "2.31.0"
51
+ backend_packages = [
52
+ "pants.backend.python",
53
+ "pants_ty",
54
+ ]
55
+ pythonpath = ["%(buildroot)s/pants-plugins"]
56
+ ```
57
+
58
+ If you accidentally copy the dev `BUILD` files too, you may see an error like:
59
+
60
+ ```text
61
+ UnrecognizedResolveNamesError: ... resolve ... pants-plugins
62
+ ```
63
+
64
+ That means your consuming repo picked up this repo's internal development resolve. Remove the
65
+ copied `BUILD` files and keep only the plugin Python modules.
66
+
67
+ ### Option 2: install as a published plugin
68
+
69
+ Once the package is published, use:
70
+
71
+ ```toml
72
+ [GLOBAL]
73
+ plugins = ["pants-ty==0.1.0"]
74
+ backend_packages = [
75
+ "pants.backend.python",
76
+ "pants_ty",
77
+ ]
78
+ ```
79
+
80
+ ## Configure
81
+
82
+ Create either `ty.toml` or add `[tool.ty]` to `pyproject.toml`.
83
+
84
+ Example:
85
+
86
+ ```toml
87
+ [tool.ty]
88
+ exclude = [".pants.d", "dist"]
89
+ ```
90
+
91
+ Pants exposes the plugin options under `[ty]`:
92
+
93
+ ```toml
94
+ [ty]
95
+ args = ["--output-format=concise"]
96
+ config_discovery = true
97
+ ```
98
+
99
+ Useful commands:
100
+
101
+ ```bash
102
+ pants help ty
103
+ pants check --only=ty ::
104
+ pants check --only=ty path/to/pkg::
105
+ ```
106
+
107
+ To skip a target:
108
+
109
+ ```python
110
+ python_sources(
111
+ name="lib",
112
+ skip_ty=True,
113
+ )
114
+ ```
115
+
116
+ ## Development
117
+
118
+ This repo uses Pants to lint, test, and dogfood the plugin itself.
119
+
120
+ ```bash
121
+ ruff check pants-plugins tests
122
+ pants test ::
123
+ pants check ::
124
+ ```
125
+
126
+ To build a distributable wheel and sdist:
127
+
128
+ ```bash
129
+ python -m build
130
+ ```
131
+
132
+ ## Repository layout
133
+
134
+ - `pants-plugins/pants_ty`: plugin source
135
+ - `tests/pants_ty`: unit and integration tests
136
+ - `pants.toml`: local development config
137
+ - `pants.ci.toml`: CI-specific Pants settings
138
+
139
+ ## Notes
140
+
141
+ - The Pants plugin API is not stable across minor versions. This repo currently targets
142
+ Pants `2.31.x`.
143
+ - The backend only resolves imports that Pants already knows through source roots and
144
+ target resolves.
@@ -0,0 +1,2 @@
1
+ from __future__ import annotations
2
+ __version__ = "0.1.0"
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+ from pants.engine.rules import Rule
6
+ from pants.engine.unions import UnionRule
7
+
8
+ from pants_ty import rules as ty_rules
9
+ from pants_ty import skip_field, subsystem
10
+
11
+
12
+ def rules() -> Iterable[Rule | UnionRule]:
13
+ return (*subsystem.rules(), *skip_field.rules(), *ty_rules.rules())
@@ -0,0 +1,291 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from collections.abc import Iterable, Sequence
5
+ from dataclasses import dataclass
6
+
7
+ from pants.backend.python.subsystems.setup import PythonSetup
8
+ from pants.backend.python.target_types import (
9
+ InterpreterConstraintsField,
10
+ PythonResolveField,
11
+ PythonSourceField,
12
+ )
13
+ from pants.backend.python.util_rules import pex_from_targets
14
+ from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
15
+ from pants.backend.python.util_rules.partition import (
16
+ _partition_by_interpreter_constraints_and_resolve,
17
+ )
18
+ from pants.backend.python.util_rules.pex import (
19
+ PexRequest,
20
+ VenvPexProcess,
21
+ VenvPexRequest,
22
+ create_pex,
23
+ create_venv_pex,
24
+ )
25
+ from pants.backend.python.util_rules.pex_environment import PexEnvironment
26
+ from pants.backend.python.util_rules.pex_from_targets import RequirementsPexRequest
27
+ from pants.backend.python.util_rules.python_sources import (
28
+ PythonSourceFilesRequest,
29
+ prepare_python_sources,
30
+ )
31
+ from pants.core.goals.check import CheckRequest, CheckResult, CheckResults
32
+ from pants.core.util_rules import config_files
33
+ from pants.core.util_rules.config_files import find_config_file
34
+ from pants.core.util_rules.external_tool import download_external_tool
35
+ from pants.core.util_rules.source_files import SourceFilesRequest, determine_source_files
36
+ from pants.engine.collection import Collection
37
+ from pants.engine.fs import MergeDigests
38
+ from pants.engine.internals.graph import resolve_coarsened_targets
39
+ from pants.engine.intrinsics import execute_process, merge_digests
40
+ from pants.engine.platform import Platform
41
+ from pants.engine.process import Process, ProcessCacheScope, execute_process_or_raise
42
+ from pants.engine.rules import Rule, collect_rules, concurrently, implicitly, rule
43
+ from pants.engine.target import (
44
+ CoarsenedTargets,
45
+ CoarsenedTargetsRequest,
46
+ FieldSet,
47
+ Target,
48
+ )
49
+ from pants.engine.unions import UnionRule
50
+ from pants.util.logging import LogLevel
51
+ from pants.util.ordered_set import FrozenOrderedSet, OrderedSet
52
+ from pants.util.strutil import pluralize
53
+
54
+ from pants_ty.skip_field import SkipTyField
55
+ from pants_ty.subsystem import Ty
56
+
57
+
58
+ def _python_version_args(
59
+ *, python_version: str | None, user_args: Sequence[str]
60
+ ) -> tuple[str, ...]:
61
+ if python_version is None:
62
+ return ()
63
+ if any(
64
+ arg.startswith("--python-version=") or arg.startswith("--target-version=")
65
+ for arg in user_args
66
+ ):
67
+ return ()
68
+ return (f"--python-version={python_version}",)
69
+
70
+
71
+ def _extra_search_path_args(source_roots: Sequence[str]) -> tuple[str, ...]:
72
+ unique_source_roots = tuple(dict.fromkeys(source_roots))
73
+ return tuple(f"--extra-search-path={source_root}" for source_root in unique_source_roots)
74
+
75
+
76
+ def _batch_input_paths(
77
+ paths: Sequence[str], *, max_paths_per_batch: int
78
+ ) -> tuple[tuple[str, ...], ...]:
79
+ if max_paths_per_batch <= 0:
80
+ raise ValueError("max_paths_per_batch must be positive")
81
+ if not paths:
82
+ return ()
83
+ return tuple(
84
+ tuple(paths[index : index + max_paths_per_batch])
85
+ for index in range(0, len(paths), max_paths_per_batch)
86
+ )
87
+
88
+
89
+ @dataclass(frozen=True)
90
+ class TyFieldSet(FieldSet):
91
+ required_fields = (PythonSourceField,)
92
+
93
+ sources: PythonSourceField
94
+ resolve: PythonResolveField
95
+ interpreter_constraints: InterpreterConstraintsField
96
+
97
+ @classmethod
98
+ def opt_out(cls, tgt: Target) -> bool:
99
+ return tgt.get(SkipTyField).value
100
+
101
+
102
+ class TyRequest(CheckRequest):
103
+ field_set_type = TyFieldSet
104
+ tool_name = Ty.options_scope
105
+
106
+
107
+ @dataclass(frozen=True)
108
+ class TyPartition:
109
+ field_sets: FrozenOrderedSet[TyFieldSet]
110
+ root_targets: CoarsenedTargets
111
+ resolve_description: str | None
112
+ interpreter_constraints: InterpreterConstraints
113
+
114
+ def description(self) -> str:
115
+ ics = str(sorted(str(c) for c in self.interpreter_constraints))
116
+ return f"{self.resolve_description}, {ics}" if self.resolve_description else ics
117
+
118
+
119
+ class TyPartitions(Collection[TyPartition]):
120
+ pass
121
+
122
+
123
+ @rule(
124
+ desc="Determine if it is necessary to partition Ty's input",
125
+ level=LogLevel.DEBUG,
126
+ )
127
+ async def ty_determine_partitions(
128
+ request: TyRequest,
129
+ ty: Ty,
130
+ python_setup: PythonSetup,
131
+ ) -> TyPartitions:
132
+ resolve_and_interpreter_constraints_to_field_sets = (
133
+ _partition_by_interpreter_constraints_and_resolve(request.field_sets, python_setup)
134
+ )
135
+
136
+ coarsened_targets = await resolve_coarsened_targets(
137
+ CoarsenedTargetsRequest(field_set.address for field_set in request.field_sets),
138
+ **implicitly(),
139
+ )
140
+ coarsened_targets_by_address = coarsened_targets.by_address()
141
+
142
+ return TyPartitions(
143
+ TyPartition(
144
+ FrozenOrderedSet(field_sets),
145
+ CoarsenedTargets(
146
+ OrderedSet(
147
+ coarsened_targets_by_address[field_set.address] for field_set in field_sets
148
+ )
149
+ ),
150
+ resolve if len(python_setup.resolves) > 1 else None,
151
+ interpreter_constraints or ty.interpreter_constraints,
152
+ )
153
+ for (resolve, interpreter_constraints), field_sets in sorted(
154
+ resolve_and_interpreter_constraints_to_field_sets.items()
155
+ )
156
+ )
157
+
158
+
159
+ @rule(desc="Typecheck one Ty partition", level=LogLevel.DEBUG)
160
+ async def ty_typecheck_partition(
161
+ partition: TyPartition,
162
+ ty: Ty,
163
+ platform: Platform,
164
+ pex_environment: PexEnvironment,
165
+ python_setup: PythonSetup,
166
+ ) -> CheckResult:
167
+ (
168
+ ty_tool,
169
+ config_files_snapshot,
170
+ root_sources,
171
+ transitive_sources,
172
+ requirements_pex,
173
+ ) = await concurrently(
174
+ download_external_tool(ty.get_request(platform)),
175
+ find_config_file(ty.config_request()),
176
+ determine_source_files(SourceFilesRequest(fs.sources for fs in partition.field_sets)),
177
+ prepare_python_sources(
178
+ PythonSourceFilesRequest(partition.root_targets.closure()), **implicitly()
179
+ ),
180
+ create_pex(
181
+ **implicitly(
182
+ RequirementsPexRequest(
183
+ (fs.address for fs in partition.field_sets),
184
+ hardcoded_interpreter_constraints=partition.interpreter_constraints,
185
+ )
186
+ )
187
+ ),
188
+ )
189
+
190
+ complete_pex_env = pex_environment.in_workspace()
191
+ requirements_venv_pex = await create_venv_pex(
192
+ VenvPexRequest(
193
+ PexRequest(
194
+ output_filename="requirements_venv.pex",
195
+ internal_only=True,
196
+ pex_path=[requirements_pex],
197
+ interpreter_constraints=partition.interpreter_constraints,
198
+ ),
199
+ complete_pex_env,
200
+ ),
201
+ **implicitly(),
202
+ )
203
+
204
+ _ = await execute_process_or_raise(
205
+ **implicitly(
206
+ VenvPexProcess(
207
+ requirements_venv_pex,
208
+ description="Force Ty requirements venv to materialize",
209
+ argv=["-c", "''"],
210
+ cache_scope=ProcessCacheScope.PER_SESSION,
211
+ )
212
+ )
213
+ )
214
+
215
+ input_digest = await merge_digests(
216
+ MergeDigests(
217
+ (
218
+ transitive_sources.source_files.snapshot.digest,
219
+ config_files_snapshot.snapshot.digest,
220
+ requirements_venv_pex.digest,
221
+ )
222
+ )
223
+ )
224
+
225
+ immutable_input_key = "__ty_tool"
226
+ exe_path = os.path.join(immutable_input_key, ty_tool.exe)
227
+ requirements_venv_path = os.path.join(
228
+ complete_pex_env.pex_root, requirements_venv_pex.venv_rel_dir
229
+ )
230
+
231
+ python_version = partition.interpreter_constraints.minimum_python_version(
232
+ python_setup.interpreter_versions_universe
233
+ )
234
+ config_args = (f"--config-file={ty.config}",) if ty.config else ()
235
+ common_args = (
236
+ "check",
237
+ *config_args,
238
+ f"--python={requirements_venv_path}",
239
+ *_python_version_args(python_version=python_version, user_args=ty.args),
240
+ *_extra_search_path_args(transitive_sources.source_roots),
241
+ *ty.args,
242
+ )
243
+ path_batches = _batch_input_paths(tuple(root_sources.snapshot.files), max_paths_per_batch=512)
244
+
245
+ if not path_batches:
246
+ return CheckResult(0, "", "", partition_description=partition.description())
247
+
248
+ batch_results = await concurrently(
249
+ execute_process(
250
+ Process(
251
+ argv=(exe_path, *common_args, *path_batch),
252
+ input_digest=input_digest,
253
+ immutable_input_digests={immutable_input_key: ty_tool.digest},
254
+ description=f"Run Ty on {pluralize(len(path_batch), 'file')}.",
255
+ level=LogLevel.DEBUG,
256
+ ),
257
+ **implicitly(),
258
+ )
259
+ for path_batch in path_batches
260
+ )
261
+
262
+ return CheckResult(
263
+ exit_code=max(result.exit_code for result in batch_results),
264
+ stdout="".join(result.stdout.decode() for result in batch_results),
265
+ stderr="".join(result.stderr.decode() for result in batch_results),
266
+ partition_description=partition.description(),
267
+ )
268
+
269
+
270
+ @rule(desc="Typecheck using Ty", level=LogLevel.DEBUG)
271
+ async def ty_typecheck(
272
+ request: TyRequest,
273
+ ty: Ty,
274
+ ) -> CheckResults:
275
+ if ty.skip:
276
+ return CheckResults([], checker_name=request.tool_name)
277
+
278
+ partitions = await ty_determine_partitions(request, **implicitly())
279
+ partitioned_results = await concurrently(
280
+ ty_typecheck_partition(partition, **implicitly()) for partition in partitions
281
+ )
282
+ return CheckResults(partitioned_results, checker_name=request.tool_name)
283
+
284
+
285
+ def rules() -> Iterable[Rule | UnionRule]:
286
+ return (
287
+ *collect_rules(),
288
+ *config_files.rules(),
289
+ *pex_from_targets.rules(),
290
+ UnionRule(CheckRequest, TyRequest),
291
+ )
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+ from pants.backend.python.target_types import (
6
+ PythonSourceTarget,
7
+ PythonSourcesGeneratorTarget,
8
+ PythonTestTarget,
9
+ PythonTestsGeneratorTarget,
10
+ PythonTestUtilsGeneratorTarget,
11
+ )
12
+ from pants.engine.rules import Rule
13
+ from pants.engine.target import BoolField
14
+ from pants.engine.unions import UnionRule
15
+
16
+
17
+ class SkipTyField(BoolField):
18
+ alias = "skip_ty"
19
+ default = False
20
+ help = "If true, don't run Ty on this target's code."
21
+
22
+
23
+ def rules() -> Iterable[Rule | UnionRule]:
24
+ return (
25
+ PythonSourcesGeneratorTarget.register_plugin_field(SkipTyField),
26
+ PythonSourceTarget.register_plugin_field(SkipTyField),
27
+ PythonTestsGeneratorTarget.register_plugin_field(SkipTyField),
28
+ PythonTestTarget.register_plugin_field(SkipTyField),
29
+ PythonTestUtilsGeneratorTarget.register_plugin_field(SkipTyField),
30
+ )
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+ from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
6
+ from pants.core.goals.resolves import ExportableTool
7
+ from pants.core.util_rules.config_files import ConfigFilesRequest
8
+ from pants.core.util_rules.external_tool import TemplatedExternalTool
9
+ from pants.engine.platform import Platform
10
+ from pants.engine.rules import Rule, collect_rules
11
+ from pants.engine.unions import UnionRule
12
+ from pants.option.option_types import (
13
+ ArgsListOption,
14
+ BoolOption,
15
+ FileOption,
16
+ SkipOption,
17
+ StrListOption,
18
+ )
19
+ from pants.util.strutil import softwrap
20
+
21
+
22
+ class Ty(TemplatedExternalTool):
23
+ options_scope = "ty"
24
+ name = "Ty"
25
+ help = softwrap(
26
+ """
27
+ The Ty Python type checker (https://docs.astral.sh/ty/).
28
+ """
29
+ )
30
+
31
+ default_version = "0.0.24"
32
+ default_known_versions = [
33
+ "0.0.24|linux_arm64|f51f5c0fcdf06f22cf74edbc15fddb2614466a3237ec12247e6aaf6215d349ee|9778744",
34
+ "0.0.24|linux_x86_64|dbb8d08643dc2ce7dee5a1c482e1ab240d4e6eb5ae7df08b77f59df8c6374014|10612104",
35
+ "0.0.24|macos_arm64|91709778a139350dc890e6048b042bc0800b04f1cb0cd4a636af22673fa84c1a|9323636",
36
+ "0.0.24|macos_x86_64|3c0f93d0b578b556f4a7765714f6e875f836f8f0d0111fdc786b2adc79110a4d|9957953",
37
+ ]
38
+ default_url_template = (
39
+ "https://github.com/astral-sh/ty/releases/download/{version}/ty-{platform}.tar.gz"
40
+ )
41
+ default_url_platform_mapping = {
42
+ "linux_arm64": "aarch64-unknown-linux-musl",
43
+ "linux_x86_64": "x86_64-unknown-linux-musl",
44
+ "macos_arm64": "aarch64-apple-darwin",
45
+ "macos_x86_64": "x86_64-apple-darwin",
46
+ }
47
+
48
+ skip = SkipOption("check")
49
+ args = ArgsListOption(example="--error-on-warning --output-format=concise")
50
+ config = FileOption(
51
+ default=None,
52
+ advanced=True,
53
+ help=softwrap(
54
+ """
55
+ Path to a `ty.toml` file to use for configuration.
56
+
57
+ Setting this option disables `[ty].config_discovery`.
58
+ """
59
+ ),
60
+ )
61
+ config_discovery = BoolOption(
62
+ default=True,
63
+ advanced=True,
64
+ help=softwrap(
65
+ """
66
+ If true, Pants will include relevant Ty config files during runs
67
+ (`ty.toml` and `pyproject.toml` containing `[tool.ty]`).
68
+ """
69
+ ),
70
+ )
71
+ _interpreter_constraints = StrListOption(
72
+ advanced=True,
73
+ default=["CPython>=3.8,<3.15"],
74
+ help="Python interpreter constraints for Ty.",
75
+ )
76
+
77
+ def generate_exe(self, plat: Platform) -> str:
78
+ return f"ty-{self.default_url_platform_mapping[plat.value]}/ty"
79
+
80
+ @property
81
+ def interpreter_constraints(self) -> InterpreterConstraints:
82
+ return InterpreterConstraints(self._interpreter_constraints)
83
+
84
+ def config_request(self) -> ConfigFilesRequest:
85
+ return ConfigFilesRequest(
86
+ specified=self.config,
87
+ specified_option_name=f"{self.options_scope}.config",
88
+ discovery=self.config_discovery,
89
+ check_existence=["ty.toml"],
90
+ check_content={"pyproject.toml": b"[tool.ty"},
91
+ )
92
+
93
+
94
+ def rules() -> Iterable[Rule | UnionRule]:
95
+ return (
96
+ *collect_rules(),
97
+ *Ty.rules(),
98
+ UnionRule(ExportableTool, Ty),
99
+ )
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: pants-ty
3
+ Version: 0.1.0
4
+ Summary: A Pants check backend for the Astral ty Python type checker.
5
+ Author: Vasile Razdalovschi
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/vrazdalovschi/pants-ty-plugin
8
+ Project-URL: Issues, https://github.com/vrazdalovschi/pants-ty-plugin/issues
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Topic :: Software Development :: Build Tools
14
+ Requires-Python: >=3.11
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: pantsbuild.pants<2.32,>=2.31
18
+ Dynamic: license-file
19
+
20
+ # pants-ty-plugin
21
+
22
+ `pants-ty-plugin` adds a Pants `check` backend for [Astral ty](https://docs.astral.sh/ty/).
23
+
24
+ It is designed for Pants `2.31.x` and currently supports Linux and macOS. The backend installs
25
+ `ty` as an external binary, then runs it with:
26
+
27
+ - a resolve-backed Python environment for third-party dependencies
28
+ - Pants source roots as `--extra-search-path` entries for first-party imports
29
+
30
+ That means `ty` can resolve imports that Pants already knows about.
31
+
32
+ ## Features
33
+
34
+ - `pants check --only=ty ...`
35
+ - `skip_ty = true` on Python targets
36
+ - `ty.toml` discovery
37
+ - `[tool.ty]` discovery from `pyproject.toml`
38
+ - resolve-aware `--python`
39
+ - source-root-aware `--extra-search-path`
40
+
41
+ ## Install
42
+
43
+ ### Option 1: vendor the plugin into a private repo
44
+
45
+ Copy only the Python package files from `pants-plugins/pants_ty/` into your repo's
46
+ `pants-plugins/pants_ty/` directory:
47
+
48
+ - `__init__.py`
49
+ - `register.py`
50
+ - `rules.py`
51
+ - `skip_field.py`
52
+ - `subsystem.py`
53
+
54
+ Do not copy this plugin repo's development-only files:
55
+
56
+ - `pants-plugins/BUILD`
57
+ - `pants-plugins/pants_ty/BUILD`
58
+ - `pants-plugins/lock.txt`
59
+ - `tests/`
60
+ - this repo's `pants.toml`, `pyproject.toml`, or GitHub workflow files
61
+
62
+ Those files are only for developing and releasing `pants-ty-plugin` itself. A consuming repo
63
+ should use its own resolves, lockfiles, and test setup.
64
+
65
+ Then configure:
66
+
67
+ ```toml
68
+ [GLOBAL]
69
+ pants_version = "2.31.0"
70
+ backend_packages = [
71
+ "pants.backend.python",
72
+ "pants_ty",
73
+ ]
74
+ pythonpath = ["%(buildroot)s/pants-plugins"]
75
+ ```
76
+
77
+ If you accidentally copy the dev `BUILD` files too, you may see an error like:
78
+
79
+ ```text
80
+ UnrecognizedResolveNamesError: ... resolve ... pants-plugins
81
+ ```
82
+
83
+ That means your consuming repo picked up this repo's internal development resolve. Remove the
84
+ copied `BUILD` files and keep only the plugin Python modules.
85
+
86
+ ### Option 2: install as a published plugin
87
+
88
+ Once the package is published, use:
89
+
90
+ ```toml
91
+ [GLOBAL]
92
+ plugins = ["pants-ty==0.1.0"]
93
+ backend_packages = [
94
+ "pants.backend.python",
95
+ "pants_ty",
96
+ ]
97
+ ```
98
+
99
+ ## Configure
100
+
101
+ Create either `ty.toml` or add `[tool.ty]` to `pyproject.toml`.
102
+
103
+ Example:
104
+
105
+ ```toml
106
+ [tool.ty]
107
+ exclude = [".pants.d", "dist"]
108
+ ```
109
+
110
+ Pants exposes the plugin options under `[ty]`:
111
+
112
+ ```toml
113
+ [ty]
114
+ args = ["--output-format=concise"]
115
+ config_discovery = true
116
+ ```
117
+
118
+ Useful commands:
119
+
120
+ ```bash
121
+ pants help ty
122
+ pants check --only=ty ::
123
+ pants check --only=ty path/to/pkg::
124
+ ```
125
+
126
+ To skip a target:
127
+
128
+ ```python
129
+ python_sources(
130
+ name="lib",
131
+ skip_ty=True,
132
+ )
133
+ ```
134
+
135
+ ## Development
136
+
137
+ This repo uses Pants to lint, test, and dogfood the plugin itself.
138
+
139
+ ```bash
140
+ ruff check pants-plugins tests
141
+ pants test ::
142
+ pants check ::
143
+ ```
144
+
145
+ To build a distributable wheel and sdist:
146
+
147
+ ```bash
148
+ python -m build
149
+ ```
150
+
151
+ ## Repository layout
152
+
153
+ - `pants-plugins/pants_ty`: plugin source
154
+ - `tests/pants_ty`: unit and integration tests
155
+ - `pants.toml`: local development config
156
+ - `pants.ci.toml`: CI-specific Pants settings
157
+
158
+ ## Notes
159
+
160
+ - The Pants plugin API is not stable across minor versions. This repo currently targets
161
+ Pants `2.31.x`.
162
+ - The backend only resolves imports that Pants already knows through source roots and
163
+ target resolves.
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ pants-plugins/pants_ty/__init__.py
5
+ pants-plugins/pants_ty/register.py
6
+ pants-plugins/pants_ty/rules.py
7
+ pants-plugins/pants_ty/skip_field.py
8
+ pants-plugins/pants_ty/subsystem.py
9
+ pants-plugins/pants_ty.egg-info/PKG-INFO
10
+ pants-plugins/pants_ty.egg-info/SOURCES.txt
11
+ pants-plugins/pants_ty.egg-info/dependency_links.txt
12
+ pants-plugins/pants_ty.egg-info/requires.txt
13
+ pants-plugins/pants_ty.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ pantsbuild.pants<2.32,>=2.31
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pants-ty"
7
+ version = "0.1.0"
8
+ description = "A Pants check backend for the Astral ty Python type checker."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{name = "Vasile Razdalovschi"}]
14
+ dependencies = ["pantsbuild.pants>=2.31,<2.32"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Topic :: Software Development :: Build Tools",
21
+ ]
22
+
23
+ [project.urls]
24
+ Repository = "https://github.com/vrazdalovschi/pants-ty-plugin"
25
+ Issues = "https://github.com/vrazdalovschi/pants-ty-plugin/issues"
26
+
27
+ [tool.setuptools]
28
+ package-dir = {"" = "pants-plugins"}
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["pants-plugins"]
32
+ include = ["pants_ty*"]
33
+
34
+ [tool.ruff]
35
+ line-length = 100
36
+ target-version = "py311"
37
+ exclude = [".git", ".pants.d", "build", "dist"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+