Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ All notable user-facing changes to `rules_pyrefly` are documented in this file.
### Added

- Support for Bazel 8.x alongside Bazel 9.x.
- Add target-local opt-in, suppression, preset, and severity configuration
through `aspect_hints` ([#2](https://github.com/facebook/rules_pyrefly/pull/2)).

[Unreleased]: https://github.com/facebook/rules_pyrefly/compare/v0.1.0...HEAD
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,56 @@ pyrefly_aspect = pyrefly(
)
```

### Target-local configuration

Use `pyrefly_hint` in a target's `aspect_hints` to opt that target in or give it
target-local configuration:

```starlark
load(
"@rules_pyrefly//pyrefly:pyrefly.bzl",
"pyrefly_hint",
)

pyrefly_hint(
name = "strict_pyrefly",
error_severities = {"bad-assignment": "error"},
min_severity = "warn",
preset = "strict",
)

py_library(
name = "library",
srcs = ["library.py"],
aspect_hints = [":strict_pyrefly"],
)
```

Unless an entry from the configured `suppression_tags` is present, a matching
hint opts the target in even when the aspect has nonempty `opt_in_tags`. Set
`suppress = True` on the hint to suppress the target instead. The hint's
nonempty `preset` and `min_severity` replace the aspect defaults, and its
`error_severities` entries are merged over the aspect-level map. A suppressing
hint cannot also set `preset`, `min_severity`, or `error_severities`.

Because `aspect_hints` is configurable, selection can depend on the target
configuration:

```starlark
py_library(
name = "library",
srcs = ["library.py"],
aspect_hints = select({
"//tools:enable_pyrefly": [":strict_pyrefly"],
"//conditions:default": [],
}),
)
```

Attach at most one hint providing `PyreflyHintInfo` to a target. Other aspect
hints are ignored. Advanced integrations may load `PyreflyHintInfo` directly
and return it from a custom lightweight hint rule.

Use `pyrefly_cli` when testing a local or otherwise preinstalled Pyrefly binary:

```starlark
Expand Down
23 changes: 10 additions & 13 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,13 @@ apply Pyrefly to existing `py_*` targets with `.bazelrc` configuration and no
BUILD-file migration. It also matches how many Bazel projects already integrate
type checkers.

The tradeoff is that configuration is mostly aspect-level rather than
target-level. That is a deliberate v1 simplification. Projects can use tags for
broad opt-in/opt-out behavior, and more granular policy can be added later if
real usage shows the need.

If users need configuration-sensitive policy later, such as checking a target on
one platform but skipping it on another, plain tags may not be expressive
enough. `aspect_hints` could be a better extension point because they can point
to labels that carry structured policy for the aspect to inspect.
Configuration defaults remain aspect-level, while `aspect_hints` provide a
target-local extension point. A matching Pyrefly hint can opt a target in,
suppress it, or override its preset and diagnostic severities. Because
`aspect_hints` is configurable, projects can use `select()` for policy such as
checking a target only in a particular build configuration. The provider-based
interface also lets other rule sets attach Pyrefly policy without adding
Pyrefly-specific attributes to their rules.

### Check Owned Files, Import Dependencies

Expand Down Expand Up @@ -182,11 +180,13 @@ escape hatch.

## Public Shape

Users configure `rules_pyrefly` in two places:
Users configure `rules_pyrefly` in three places:

- A Bzlmod extension tag selects the Pyrefly binary version and optional release
asset checksums.
- An aspect factory configures the checking policy for a build.
- Target-local `aspect_hints` opt targets in, suppress them, or override preset
and severity settings.

The aspect factory exposes only Bazel-relevant knobs:

Expand Down Expand Up @@ -214,9 +214,6 @@ The detailed contract tests for the Bazel/Pyrefly boundary are tracked in
These areas are intentionally left for later versions unless early users show
they are needed sooner:

- Per-target severity overrides.
- Structured or configuration-sensitive target policy, possibly through
`aspect_hints`.
- First-class gradual Pyrefly version rollout.
- Cross-target Pyrefly cache propagation.
- Exact `py.typed` package-data modeling.
Expand Down
29 changes: 20 additions & 9 deletions docs/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,25 @@ needs in a stable shape.

The aspect traverses `deps` and uses `required_providers = [PyInfo]` so Bazel
does not invoke it on non-Python targets. A target is checked only when it is in
the root workspace, it passes the configured tag policy, and it owns at least
one direct Python source or stub after helper paths such as `/_virtual_imports/`
are filtered out.

Tag policy is intentionally simple: `suppression_tags` wins first, then
`opt_in_tags` is checked. With no opt-in tags, all otherwise eligible Python
targets are checked. The tag policy and initial target-selection structure were
adapted from [rules_mypy](https://github.com/bazel-contrib/rules_mypy).
the root workspace, it passes the configured selection policy, and it owns at
least one direct Python source or stub after helper paths such as
`/_virtual_imports/` are filtered out.

The aspect reads `PyreflyHintInfo` providers from the visited target's
`aspect_hints`. It ignores hints that do not provide `PyreflyHintInfo` and fails
when a target attaches more than one matching hint, avoiding order-dependent
configuration. For targets in the root workspace, this validation happens
before the suppression and opt-in policy is applied. External-repository
targets are skipped without validating their hints.

After validation, `suppression_tags` wins first. A matching hint with
`suppress = True` also suppresses checking. Any other matching hint opts the
target in, including when it has none of the configured `opt_in_tags`. With no
opt-in tags or hints, all otherwise eligible Python targets are checked. A
hint's nonempty preset and minimum severity override the aspect defaults, and
its per-error severity entries merge over the aspect-level map. The initial tag
policy was adapted from
[rules_mypy](https://github.com/bazel-contrib/rules_mypy).

The check roots are the target-owned files from
`PyInfo.direct_original_sources` and `PyInfo.direct_pyi_files`. Dependency-owned
Expand Down Expand Up @@ -73,7 +84,7 @@ The top-level sections are:
- `path_overlays`: sparse file facts for transitive `.py` and `.pyi` files
whose readable action path differs from their `short_path`.
- `config`: modeled Python environment and Pyrefly configuration passed through
the aspect.
the aspect, including target-local hint overrides.

The shape is intentionally coarse-grained:

Expand Down
2 changes: 2 additions & 0 deletions docs/users.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Bazel Python project.
- Keep the same opt-out tags that the project already uses for type checking,
such as `no-mypy` or `no-checks`, so generated or intentionally skipped
targets stay skipped.
- Use `pyrefly_hint` through `aspect_hints` when selection must use `select()` or
a target needs different preset or severity settings from the aspect defaults.

## Running Checks

Expand Down
56 changes: 55 additions & 1 deletion examples/demo/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.

load(":generated_file.bzl", "generated_python_file")
load("@rules_pyrefly//pyrefly:pyrefly.bzl", "pyrefly_hint")
load("@rules_python//python:py_binary.bzl", "py_binary")
load("@rules_python//python:py_library.bzl", "py_library")
load("@rules_python//python:py_test.bzl", "py_test")
load(":generated_file.bzl", "generated_python_file")
load(":hint_test.bzl", "partial_pyrefly_hint")

py_library(
name = "hello",
Expand All @@ -29,6 +31,58 @@ py_library(
tags = ["no-pyrefly"],
)

pyrefly_hint(
name = "suppression_hint",
suppress = True,
)

py_library(
name = "hint_suppressed",
srcs = ["hint_suppressed.py"],
aspect_hints = [":suppression_hint"],
)

pyrefly_hint(
name = "configured_hint",
error_severities = {"missing-attribute": "info"},
min_severity = "warn",
preset = "strict",
)

py_library(
name = "hint_configured",
srcs = ["hint_configured.py"],
aspect_hints = [":configured_hint"],
)

partial_pyrefly_hint(
name = "partial_custom_hint",
)

py_library(
name = "custom_hint_checked",
srcs = ["custom_hint_checked.py"],
aspect_hints = [":partial_custom_hint"],
)

config_setting(
name = "enable_conditional_hint",
define_values = {"pyrefly_hint": "enabled"},
)

pyrefly_hint(
name = "conditional_hint",
)

py_library(
name = "conditionally_checked",
srcs = ["conditionally_checked.py"],
aspect_hints = select({
":enable_conditional_hint": [":conditional_hint"],
"//conditions:default": [],
}),
)

py_library(
name = "with_stub",
srcs = ["with_stub.py"],
Expand Down
6 changes: 6 additions & 0 deletions examples/demo/aspects_test.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,11 @@
load("@rules_pyrefly//pyrefly:pyrefly.bzl", "pyrefly")

pyrefly_aspect = pyrefly(
error_severities = {"bad-return": "warn"},
pyrefly_cli = Label("//:fake_pyrefly"),
)

pyrefly_opt_in_aspect = pyrefly(
opt_in_tags = ["pyrefly"],
pyrefly_cli = Label("//:fake_pyrefly"),
)
15 changes: 15 additions & 0 deletions examples/demo/conditionally_checked.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

VALUE: int = 1
15 changes: 15 additions & 0 deletions examples/demo/custom_hint_checked.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

VALUE: int = 1
13 changes: 9 additions & 4 deletions examples/demo/fake_pyrefly.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,18 @@ def parse_args() -> argparse.Namespace:

def main() -> int:
args = parse_args()
if args.min_severity != "error":
data = json.loads(args.input.read_text(encoding="utf-8"))
target_name = data["target"]["name"]
expected_min_severity = (
"warn" if target_name == "hint_configured" else "error"
)
if args.min_severity != expected_min_severity:
raise ValueError(
f"expected --min-severity error, got {args.min_severity!r}"
"expected --min-severity "
f"{expected_min_severity}, got {args.min_severity!r}"
)

data = json.loads(args.input.read_text(encoding="utf-8"))
if data["target"]["name"] == "type_error":
if target_name == "type_error":
print(
"type_error.py:1:14 Incompatible assignment [bad-assignment]",
file=sys.stderr,
Expand Down
15 changes: 15 additions & 0 deletions examples/demo/hint_configured.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

VALUE: int = 1
15 changes: 15 additions & 0 deletions examples/demo/hint_suppressed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

VALUE: int = 1
25 changes: 25 additions & 0 deletions examples/demo/hint_test.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Custom Pyrefly aspect hints used by integration tests."""

load("@rules_pyrefly//pyrefly:pyrefly.bzl", "PyreflyHintInfo")

def _partial_hint_impl(_ctx):
return [PyreflyHintInfo(preset = "")]

partial_pyrefly_hint = rule(
implementation = _partial_hint_impl,
provides = [PyreflyHintInfo],
)
Loading
Loading