Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
ad6674f
Add rules_pyrefly static type checking for //python/runfiles:runfiles
rickeylev Aug 6, 2026
f06e59f
Move Pyrefly aspect to tools/private/pyrefly.bzl
rickeylev Aug 6, 2026
fb6a531
Move Pyrefly aspect into tools/private/pyrefly directory
rickeylev Aug 6, 2026
286d28f
Update tools/private/pyrefly/BUILD.bazel via Gazelle
rickeylev Aug 6, 2026
4b15b38
Use //tools/private/pyrefly:pyrefly.bzl%pyrefly_aspect label in .bazelrc
rickeylev Aug 6, 2026
3f383d1
Remove leftover pyrefly bzl_library targets from tools and tools/priv…
rickeylev Aug 6, 2026
2e10394
Remove tools/private/pyrefly/BUILD.bazel target
rickeylev Aug 6, 2026
15454f9
Add pyrefly_check rule under //tests/runfiles for target-integrated t…
rickeylev Aug 6, 2026
7e1644e
Restore tools/private/pyrefly/BUILD.bazel with Gazelle formatting and…
rickeylev Aug 6, 2026
1cd767a
Remove redundant build:pyrefly config from .bazelrc
rickeylev Aug 6, 2026
bd1c5fb
Mark pyrefly extension/toolchain as dev_dependency in MODULE.bazel an…
rickeylev Aug 6, 2026
1f8e8b1
Convert pyrefly_check to test rule pyrefly_check_test
rickeylev Aug 6, 2026
7028d12
Wrap pyrefly_check in build_test in tests/runfiles/BUILD.bazel
rickeylev Aug 6, 2026
4ab6a4b
Hide build_test behind pyrefly_check_test macro
rickeylev Aug 6, 2026
5d5fae7
Move tools/private/pyrefly to tests/support/pyrefly
rickeylev Aug 6, 2026
4dd0dfc
Merge upstream/main into use_pyrefly_runfiles_check
rickeylev Aug 6, 2026
6a5b071
Use tag-based Pyrefly opt-in with global aspect registration
rickeylev Aug 6, 2026
e63101e
Gate Pyrefly aspect evaluation behind BZLMOD_ENABLED
rickeylev Aug 6, 2026
d14c712
Move BZLMOD_ENABLED check into pyrefly_check_test macro
rickeylev Aug 6, 2026
0b62db9
Declare rules_pyrefly in internal_dev_deps for WORKSPACE mode
rickeylev Aug 6, 2026
10f59a0
Add comment explaining early exit when BZLMOD_ENABLED is False
rickeylev Aug 6, 2026
43649e1
Use global aspect and WORKSPACE stub for rules_pyrefly
rickeylev Aug 7, 2026
bd8dad9
Remove pyrefly_check_test macro invocation from tests/runfiles
rickeylev Aug 7, 2026
d4f11d6
Remove pyrefly_check_test macro and pyrefly_check rule
rickeylev Aug 7, 2026
47978ca
Add no_gh_auth_token_in_cmdline workspace rule
rickeylev Aug 7, 2026
adeaadc
Merge remote-tracking branch 'upstream/main' into enable_pyrefly_pyth…
rickeylev Aug 7, 2026
b0bff18
Enable opt-out Pyrefly static type checking across Python targets
rickeylev Aug 7, 2026
6ce8842
Enable Pyrefly static type checking across remaining Python targets
rickeylev Aug 8, 2026
6a7d66e
Merge remote-tracking branch 'upstream/main' into enable_pyrefly_pyth…
rickeylev Aug 8, 2026
aa19cec
test(repl): restore test_module import in test_cannot_import_test_mod…
rickeylev Aug 8, 2026
302feb7
docs(plan): document explanations for retained no-pyrefly targets
rickeylev Aug 8, 2026
385d25a
feat(tests): enable pyrefly on bootstrap, build_data, py_extension, a…
rickeylev Aug 8, 2026
04bbbac
test(venv_site_packages_libs): assert ELFFile and MachO in shared_lib…
rickeylev Aug 8, 2026
d1d1e75
docs: remove temporary Pyrefly enablement plan
rickeylev Aug 8, 2026
8875ba6
Merge remote-tracking branch 'upstream/main' into enable_pyrefly_pyth…
rickeylev Aug 8, 2026
7b309e9
fix(typing): resolve pyrefly typing diagnostics in console script gen…
rickeylev Aug 8, 2026
9fd9cd7
fix(typing): safely access sys._base_executable in bootstrap test
rickeylev Aug 8, 2026
b42658f
fix(typing): assert valid runfiles and rlocation in repl template
rickeylev Aug 8, 2026
6c80f6a
fix(typing): fix pyrefly diagnostics in test validator, runner, and u…
rickeylev Aug 8, 2026
7a38262
fix(typing): address Pyrefly static type analysis diagnostics across …
rickeylev Aug 8, 2026
1bdfc13
fix(typing): ignore WSGI application argument type for make_server in…
rickeylev Aug 8, 2026
aea41d2
fix(examples): restore clean wheel packaging by setting no-pyrefly on…
rickeylev Aug 8, 2026
1ee9f20
fix(typing): add runfiles non-null assertions in abi3_headers_linkage…
rickeylev Aug 8, 2026
7e4f096
refactor: address review feedback on type checking and runfiles
rickeylev Aug 9, 2026
c46923e
fix(sphinxdocs): add pyrefly ignores and fix parameter name in bzl.py
rickeylev Aug 9, 2026
2ca0541
fix(release): ignore bad-argument-type in mock_gh resolve_merge_commi…
rickeylev Aug 9, 2026
656fcfc
Merge remote-tracking branch 'upstream/main' into enable_pyrefly_pyth…
rickeylev Aug 9, 2026
433ae08
Address review comments on Pyrefly annotations and ignores
rickeylev Aug 9, 2026
9e163c6
Fix mypy errors in runfiles library and tests
rickeylev Aug 9, 2026
dd1a434
Fix return type annotation for Path.runfiles_root
rickeylev Aug 9, 2026
bfcd6d0
Address review feedback on runfiles, repl, and docs
rickeylev Aug 9, 2026
9157f96
Use getattr in Path._make_child to avoid mypy superclass error
rickeylev Aug 9, 2026
89d4498
Simplify super()._make_child and clean debuggers doc example
rickeylev Aug 9, 2026
21d24af
Explain type ignores on Path._make_child
rickeylev Aug 9, 2026
e09a898
Merge branch 'upstream/main' into enable_pyrefly_python_targets
rickeylev Aug 10, 2026
2685982
Clarify error-specific ignores rule in python.md
rickeylev Aug 10, 2026
04d312c
Fix Sphinx method override signatures and remove bad-override disables
rickeylev Aug 10, 2026
53ad990
Restore pyrefly bad-override ignores in sphinxdocs and align signode …
rickeylev Aug 10, 2026
552dc57
Add explanatory comments to pyrefly bad-override ignore suppressions
rickeylev Aug 10, 2026
78a20cf
Restore NOTE comment in sphinx_bzl/BUILD.bazel
rickeylev Aug 10, 2026
9aaef3a
Use collections.abc and builtin types instead of typing aliases
rickeylev Aug 10, 2026
6af1a5f
Replace Optional[X] with X | None
rickeylev Aug 10, 2026
f43c5ad
Clean up request arguments access, runfiles init, and Sequence typing
rickeylev Aug 10, 2026
2e43776
Enable future annotations in pathlib_test.py
rickeylev Aug 10, 2026
d4dc460
Merge branch 'upstream/main' into enable_pyrefly_python_targets
rickeylev Aug 10, 2026
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
10 changes: 10 additions & 0 deletions .agents/rules/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@
* **External Objects**: When defining a `TypedDict` for an external object,
link to its definition in the docstring.

## Type Checking & Annotations
* **In-file disables vs target skipping**: Prefer `# pyrefly: ignore[<error-code>]`
(e.g. `[missing-import]`) over `tags = ["no-pyrefly"]`.
* **No blanket ignores**: NEVER use bare `# type: ignore` or literal
`# type: ignore[...]`. Use error-specific ignores instead.
* **Type assertions**: When adding assertions for type narrowing, add an
end-of-line comment: `assert foo is not None # type assert`.
Comment thread
rickeylev marked this conversation as resolved.
* **Consent for `Any`**: Require user consent before changing type annotations
to `Any`.

## Delegating Functions
* Module-level functions delegating to class methods should have a docstring
referring to the class method (e.g. `"""Refer to \`Class.method\`."""`).
Expand Down
105 changes: 87 additions & 18 deletions .agents/skills/analyze-ci-failure/scripts/analyze_ci_failure.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,29 @@
import urllib.request


def fetch_log(build_id, job_id, output_path):
def fetch_log(job_name, build_id, job_id, output_path):
if (
"readthedocs" in job_name.lower()
or "readthedocs" in build_id.lower()
or "readthedocs" in job_id.lower()
):
rtd_match = re.search(r"(\d+)", build_id) or re.search(r"(\d+)", job_id)
if rtd_match:
rtd_id = rtd_match.group(1)
rtd_url = f"https://app.readthedocs.org/api/v2/build/{rtd_id}.txt"
print(f"📥 Downloading ReadTheDocs failure log from {rtd_url}...")
req = urllib.request.Request(rtd_url, headers={"User-Agent": "ci-analyzer"})
try:
with urllib.request.urlopen(req) as resp:
content = resp.read()
with open(output_path, "wb") as f:
f.write(content)
return True
except Exception as e:
print(
f"⚠️ Failed to download RTD log from {rtd_url}: {e}", file=sys.stderr
)

if build_id.startswith("http"):
log_url = build_id
elif job_id.startswith("http"):
Expand All @@ -17,9 +39,10 @@ def fetch_log(build_id, job_id, output_path):
log_url = f"https://buildkite.com/organizations/bazel/pipelines/rules-python-python/builds/{build_id}/jobs/{job_id}/download.txt"

# Check if this is a GitHub Actions job
gh_match = re.search(r"github\.com/.*/job/(\d+)", log_url) or re.search(
r"^(\d+)$", job_id
)
gh_match = re.search(r"github\.com/.*/job/(\d+)", log_url)
if not gh_match and "github" in job_name.lower() and re.match(r"^\d+$", job_id):
gh_match = re.match(r"^(\d+)$", job_id)

if gh_match:
gh_job_id = gh_match.group(1)
print(f"📥 Fetching GitHub Action log for job {gh_job_id} using gh CLI...")
Expand Down Expand Up @@ -53,6 +76,9 @@ def fetch_log(build_id, job_id, output_path):
return False


ANSI_ESCAPE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")


def parse_log(log_path):
if not os.path.exists(log_path):
return [f"Log file not found at {log_path}"]
Expand All @@ -62,28 +88,36 @@ def parse_log(log_path):

errors = []
for line in lines:
clean_line = ANSI_ESCAPE.sub("", line).strip()
# Clean buildkite timestamp prefix: _bk;t=...
clean_line = re.sub(r"^_bk;t=\d+\s*", "", clean_line)
if any(
keyword in line
keyword.lower() in clean_line.lower()
for keyword in [
"ERROR:",
"FAILED:",
"Critical Path",
"Traceback",
"Exception",
"FileNotFoundError",
"error:",
"failed:",
"critical path",
"traceback",
"exception",
"filenotfounderror",
"no such package",
"no such target",
"exit code",
"exit-code",
"status 125",
"fatal:",
"fatal",
"##[error]",
"Would reformat:",
"would reformat:",
"would be reformatted",
"error]",
"error waiting for container",
"error during connect:",
"user command error:",
]
):
errors.append(line.strip())
if clean_line:
errors.append(clean_line)

return errors[:30]

Expand All @@ -95,8 +129,44 @@ def create_plan(job_name, log_path, errors):
else "No obvious keyword error lines matched. Please inspect the raw log file."
)

is_flake = False
flake_reason = ""
if any(
"fatal: destination path '.' already exists and is not an empty directory." in e
for e in errors
):
is_flake = True
flake_reason = "ReadTheDocs workspace checkout race / dirty container environment where target directory is not empty (`fatal: destination path '.' already exists`). This is an infrastructure flake, not a codebase failure."
elif any("exit code 2" in e.lower() for e in errors) and (
"docs" in job_name.lower() or "readthedocs" in job_name.lower()
):
is_flake = True
flake_reason = "Known docs build flake with exit code 2."
elif any(
"error waiting for container" in e.lower()
or "status 125" in e.lower()
or "error during connect:" in e.lower()
or "docker-buildkite-plugin command hook exited with status 125" in e.lower()
for e in errors
):
is_flake = True
flake_reason = "Buildkite agent / Docker runner infrastructure failure (dockerd disconnection / grpc context canceled / exit status 125). This is an infrastructure flake, not a codebase bug."

classification = (
"⚡ **Classification**: **Infrastructure / Flake Issue** (Not a codebase bug)"
if is_flake
else "🔍 **Classification**: **Code / Configuration Issue**"
)
fix_advice = (
f"Retry the failed job (`buildkite-retry-job`). {flake_reason}"
if is_flake
else "Resolve the root cause in the relevant source / build files."
)

plan = f"""# 🚨 CI Failure Analysis Report: {job_name}

{classification}

## 📁 CI Log Path
`{log_path}`

Expand All @@ -106,10 +176,9 @@ def create_plan(job_name, log_path, errors):
```

## 🛠️ Suggested Plan to Fix
1. **Inspect Log**: Review the exact log snippets above or read the full raw log file at `{log_path}`.
2. **Reproduce Locally**: Run `./replicate_ci "{job_name}"` or the matching `bazel build/test` command locally.
3. **Apply Fix**: Resolve the root cause in the relevant `BUILD.bazel` or Starlark files.
4. **Verify & Push**: Run local verification with `--config=fast-tests` and push the updated branch to trigger a clean pipeline.
1. **Diagnosis**: {flake_reason if is_flake else "Review extracted errors."}
2. **Action**: {fix_advice}
3. **Verify**: Check the new build status once re-triggered.
"""
return plan

Expand All @@ -131,7 +200,7 @@ def main():
safe_jname = re.sub(r"[^a-zA-Z0-9]", "_", args.job_name)
log_path = os.path.join(scratch_dir, f"ci_{safe_jname}_{args.job_id}.log")

fetch_log(args.build_id, args.job_id, log_path)
fetch_log(args.job_name, args.build_id, args.job_id, log_path)

print(f"🚀 Analyzing CI failure log for '{args.job_name}' at '{log_path}'...")
errors = parse_log(log_path)
Expand Down
4 changes: 3 additions & 1 deletion .bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ test --test_output=errors
# Python targets as required.
build --incompatible_default_to_explicit_init_py
build --//python/config_settings:incompatible_default_to_explicit_init_py=True
build --aspects=//tests/support/pyrefly:pyrefly.bzl%pyrefly_aspect
build --config=pyrefly
build:pyrefly --aspects=//tests/support/pyrefly:pyrefly.bzl%pyrefly_aspect
build:pyrefly --output_groups=+pyrefly

# Ensure ongoing compatibility with this flag.
common --incompatible_disallow_struct_provider_syntax
Expand Down
4 changes: 2 additions & 2 deletions docs/howto/debuggers.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,10 @@ For the remainder of this document, we assume you are using vscode.
# Import debugpy, provided by VS Code
try:
# debugpy._vendored is needed for force_pydevd to perform path manipulation.
import debugpy._vendored # type: ignore[import-not-found]
import debugpy._vendored

# pydev_monkey patches os and subprocess functions to handle new launched processes.
from _pydev_bundle import pydev_monkey # type: ignore[import-not-found]
from _pydev_bundle import pydev_monkey
except ImportError as exc:
print(f"Error: This script must be run via VS Code's debug adapter. Details: {exc}")
sys.exit(-1)
Expand Down
6 changes: 3 additions & 3 deletions examples/wheel/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import examples.wheel.lib.module_with_data as module_with_data
import examples.wheel.lib.module_with_type_annotations as module_with_type_annotations
import examples.wheel.lib.simple_module as simple_module
import examples.wheel.lib.module_with_data as module_with_data # pyrefly: ignore[missing-import]
import examples.wheel.lib.module_with_type_annotations as module_with_type_annotations # pyrefly: ignore[missing-import]
import examples.wheel.lib.simple_module as simple_module # pyrefly: ignore[missing-import]


def function():
Expand Down
3 changes: 1 addition & 2 deletions examples/wheel/private/directory_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@
import argparse
import json
from pathlib import Path
from typing import Tuple


def _file_input(value) -> Tuple[Path, str]:
def _file_input(value) -> tuple[Path, str]:
path, content = value.split("=", maxsplit=1)
return (Path(path), json.loads(content))

Expand Down
17 changes: 9 additions & 8 deletions examples/wheel/wheel_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def setUp(self):
self.runfiles = runfiles.Create()

def _get_path(self, filename):
assert self.runfiles is not None # type assert
runfiles_path = os.path.join("rules_python/examples/wheel", filename)
path = self.runfiles.Rlocation(runfiles_path)
# The runfiles API can return None if the path doesn't exist or
Expand Down Expand Up @@ -110,7 +111,7 @@ def test_py_package_wheel(self):
],
)
self.assertFileSha256Equal(
filename, "39bec133cf79431e8d057eae550cd91aa9dfbddfedb53d98ebd36e3ade2753d0"
filename, "7322902ab63fd702afb9730843496637058b5d7449208c624875d06d191d386e"
)

def test_customized_wheel(self):
Expand Down Expand Up @@ -155,7 +156,7 @@ def test_customized_wheel(self):
examples/wheel/lib/module_with_type_annotations.py,sha256=2p_0YFT0TBUufbGCAR_u2vtxF1nM0lf3dX4VGeUtYq0,637
examples/wheel/lib/module_with_type_annotations.pyi,sha256=fja3ql_WRJ1qO8jyZjWWrTTMcg1J7EpOQivOHY_8vI4,630
examples/wheel/lib/simple_module.py,sha256=z2hwciab_XPNIBNH8B1Q5fYgnJvQTeYf0ZQJpY8yLLY,637
examples/wheel/main.py,sha256=mFiRfzQEDwCHr-WVNQhOH26M42bw1UMF6IoqvtuDTrw,1047
examples/wheel/main.py,sha256=THX1qSP_5NUcJrzcFFtpCO7XKFFgPpvanwZo4X_1e-o,1152
example_customized-0.0.1.dist-info/WHEEL,sha256=sobxWSyDDkdg_rinUth-jxhXHqoNqlmNMJY3aTZn2Us,91
example_customized-0.0.1.dist-info/METADATA,sha256=QYQcDJFQSIqan8eiXqL67bqsUfgEAwf2hoK_Lgi1S-0,559
example_customized-0.0.1.dist-info/entry_points.txt,sha256=pqzpbQ8MMorrJ3Jp0ntmpZcuvfByyqzMXXi2UujuXD0,137
Expand Down Expand Up @@ -206,7 +207,7 @@ def test_customized_wheel(self):
second = second.main:s""",
)
self.assertFileSha256Equal(
filename, "685f68fc6665f53c9b769fd1ba12cce9937ab7f40ef4e60c82ef2de8653935de"
filename, "6d08fbb30864cee89396e7857c910c92bec56b6586d40a64b796b4812af15fbf"
)

def test_filename_escaping(self):
Expand Down Expand Up @@ -278,7 +279,7 @@ def test_custom_package_root_wheel(self):
for line in record_contents.splitlines():
self.assertFalse(line.startswith("/"))
self.assertFileSha256Equal(
filename, "2fbfc3baaf6fccca0f97d02316b8344507fe6c8136991a66ee5f162235adb19f"
filename, "0b5a35251ad35fd9e14f3f7e77993f59a7341268f24fb0a255b403cee60d429e"
)

def test_custom_package_root_multi_prefix_wheel(self):
Expand Down Expand Up @@ -312,7 +313,7 @@ def test_custom_package_root_multi_prefix_wheel(self):
for line in record_contents.splitlines():
self.assertFalse(line.startswith("/"))
self.assertFileSha256Equal(
filename, "3e67971ca1e8a9ba36a143df7532e641f5661c56235e41d818309316c955ba58"
filename, "437127690584a035dc37542f64c38d1a6d6652655afc81f5a9472706343aae23"
)

def test_custom_package_root_multi_prefix_reverse_order_wheel(self):
Expand Down Expand Up @@ -346,7 +347,7 @@ def test_custom_package_root_multi_prefix_reverse_order_wheel(self):
for line in record_contents.splitlines():
self.assertFalse(line.startswith("/"))
self.assertFileSha256Equal(
filename, "372ef9e11fb79f1952172993718a326b5adda192d94884b54377c34b44394982"
filename, "265cc2ba4c99d0b62f1922f357de961f15f416bcee93ff307e4d4f04e4c067a3"
)

def test_python_requires_wheel(self):
Expand All @@ -371,7 +372,7 @@ def test_python_requires_wheel(self):
""",
)
self.assertFileSha256Equal(
filename, "10a325ba8f77428b5cfcff6345d508f5eb77c140889eb62490d7382f60d4ebfe"
filename, "cb1d0bf64df1cbf23b7d4473a1c113cedbbea0d23209cdb4d2e5fe2edc68ceec"
)

def test_python_abi3_binary_wheel(self):
Expand Down Expand Up @@ -436,7 +437,7 @@ def test_rule_creates_directory_and_is_included_in_wheel(self):
],
)
self.assertFileSha256Equal(
filename, "85e44c43cc19ccae9fe2e1d629230203aa11791bed1f7f68a069fb58d1c93cd2"
filename, "2358a8ee58dd7ed1a89862e368a0eb00e83ec5de28995ecf0f3c38c2524102dc"
)

def test_rule_expands_workspace_status_keys_in_wheel_metadata(self):
Expand Down
5 changes: 3 additions & 2 deletions python/bin/repl_stub.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,10 @@ def complete(self, text, state):

# TODO(jpwoodbu): Use readline.backend instead of readline.__doc__ once we can depend on having
# Python >=3.13.
if "libedit" in readline.__doc__: # type: ignore
doc = readline.__doc__ or ""
if "libedit" in doc:
readline.parse_and_bind("bind ^I rl_complete")
elif "GNU readline" in readline.__doc__: # type: ignore
elif "GNU readline" in doc:
readline.parse_and_bind("tab: complete")
else:
print("Could not enable tab completion: unable to determine readline backend")
Expand Down
9 changes: 5 additions & 4 deletions python/private/py_console_script_gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
raise

if __name__ == "__main__":
sys.exit({entry_point}()) # type: ignore
sys.exit({entry_point}()) # pyrefly: ignore[not-callable]
"""


Expand All @@ -69,10 +69,11 @@ class EntryPointsParser(configparser.ConfigParser):
See https://packaging.python.org/en/latest/specifications/entry-points/
"""

optionxform = staticmethod(str)
def optionxform(self, optionstr: str) -> str:
return str(optionstr)


def _guess_entry_point(guess: str, console_scripts: dict[string, string]) -> str | None: # noqa: F821
def _guess_entry_point(guess: str, console_scripts: dict[str, str]) -> str | None:
for key, candidate in console_scripts.items():
if guess == key:
return candidate
Expand All @@ -82,7 +83,7 @@ def run(
*,
entry_points: pathlib.Path,
out: pathlib.Path,
console_script: str,
console_script: str | None,
console_script_guess: str,
shebang: str,
):
Expand Down
Loading