diff --git a/.agents/rules/python.md b/.agents/rules/python.md index d683152814..c149130fe2 100644 --- a/.agents/rules/python.md +++ b/.agents/rules/python.md @@ -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[]` + (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`. +* **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\`."""`). diff --git a/.agents/skills/analyze-ci-failure/scripts/analyze_ci_failure.py b/.agents/skills/analyze-ci-failure/scripts/analyze_ci_failure.py old mode 100644 new mode 100755 index 661a427cc7..5708f6d1c2 --- a/.agents/skills/analyze-ci-failure/scripts/analyze_ci_failure.py +++ b/.agents/skills/analyze-ci-failure/scripts/analyze_ci_failure.py @@ -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"): @@ -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...") @@ -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}"] @@ -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] @@ -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}` @@ -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 @@ -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) diff --git a/.bazelrc b/.bazelrc index 90f3bc8fdb..cd41961452 100644 --- a/.bazelrc +++ b/.bazelrc @@ -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 diff --git a/docs/howto/debuggers.md b/docs/howto/debuggers.md index 199a366675..2fd8dada57 100644 --- a/docs/howto/debuggers.md +++ b/docs/howto/debuggers.md @@ -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) diff --git a/examples/wheel/main.py b/examples/wheel/main.py index 37b4f69811..5b221542c3 100644 --- a/examples/wheel/main.py +++ b/examples/wheel/main.py @@ -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(): diff --git a/examples/wheel/private/directory_writer.py b/examples/wheel/private/directory_writer.py index 4b69f3a5d0..d2297124cf 100644 --- a/examples/wheel/private/directory_writer.py +++ b/examples/wheel/private/directory_writer.py @@ -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)) diff --git a/examples/wheel/wheel_test.py b/examples/wheel/wheel_test.py index 8dcad42138..d289cb7c8a 100644 --- a/examples/wheel/wheel_test.py +++ b/examples/wheel/wheel_test.py @@ -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 @@ -110,7 +111,7 @@ def test_py_package_wheel(self): ], ) self.assertFileSha256Equal( - filename, "39bec133cf79431e8d057eae550cd91aa9dfbddfedb53d98ebd36e3ade2753d0" + filename, "7322902ab63fd702afb9730843496637058b5d7449208c624875d06d191d386e" ) def test_customized_wheel(self): @@ -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 @@ -206,7 +207,7 @@ def test_customized_wheel(self): second = second.main:s""", ) self.assertFileSha256Equal( - filename, "685f68fc6665f53c9b769fd1ba12cce9937ab7f40ef4e60c82ef2de8653935de" + filename, "6d08fbb30864cee89396e7857c910c92bec56b6586d40a64b796b4812af15fbf" ) def test_filename_escaping(self): @@ -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): @@ -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): @@ -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): @@ -371,7 +372,7 @@ def test_python_requires_wheel(self): """, ) self.assertFileSha256Equal( - filename, "10a325ba8f77428b5cfcff6345d508f5eb77c140889eb62490d7382f60d4ebfe" + filename, "cb1d0bf64df1cbf23b7d4473a1c113cedbbea0d23209cdb4d2e5fe2edc68ceec" ) def test_python_abi3_binary_wheel(self): @@ -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): diff --git a/python/bin/repl_stub.py b/python/bin/repl_stub.py index 858cf810b9..bb08ab2f87 100644 --- a/python/bin/repl_stub.py +++ b/python/bin/repl_stub.py @@ -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") diff --git a/python/private/py_console_script_gen.py b/python/private/py_console_script_gen.py index 2be986f732..887441c503 100644 --- a/python/private/py_console_script_gen.py +++ b/python/private/py_console_script_gen.py @@ -59,7 +59,7 @@ raise if __name__ == "__main__": - sys.exit({entry_point}()) # type: ignore + sys.exit({entry_point}()) # pyrefly: ignore[not-callable] """ @@ -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 @@ -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, ): diff --git a/python/private/py_test_main_validator.py b/python/private/py_test_main_validator.py index e3849c5f57..e66bf6849e 100644 --- a/python/private/py_test_main_validator.py +++ b/python/private/py_test_main_validator.py @@ -24,25 +24,28 @@ import ast import sys -# Statement node types that never run any code on their own, regardless of -# their contents. A module whose top-level body consists solely of these (and -# inert assignments/expressions/guards, see below) is considered inert. -_INERT_NODE_TYPES = [ - ast.FunctionDef, - ast.AsyncFunctionDef, - ast.ClassDef, - ast.Import, - ast.ImportFrom, - ast.Global, - ast.Pass, -] - -# `ast.TypeAlias` (PEP 695, e.g. `type Alias = int`) only exists on Python -# 3.12+. Add it dynamically so the validator still imports on older versions. -if hasattr(ast, "TypeAlias"): - _INERT_NODE_TYPES.append(ast.TypeAlias) - -_INERT_NODE_TYPES = tuple(_INERT_NODE_TYPES) + +def _compute_inert_node_types() -> tuple[type[ast.AST], ...]: + # Statement node types that never run any code on their own, regardless of + # their contents. A module whose top-level body consists solely of these (and + # inert assignments/expressions/guards, see below) is considered inert. + node_types: list[type[ast.AST]] = [ + ast.FunctionDef, + ast.AsyncFunctionDef, + ast.ClassDef, + ast.Import, + ast.ImportFrom, + ast.Global, + ast.Pass, + ] + # `ast.TypeAlias` (PEP 695, e.g. `type Alias = int`) only exists on Python + # 3.12+. Add it dynamically so the validator still imports on older versions. + if hasattr(ast, "TypeAlias"): + node_types.append(ast.TypeAlias) + return tuple(node_types) + + +_INERT_NODE_TYPES = _compute_inert_node_types() # `ast.TryStar` (PEP 654, `try/except*`) only exists on Python 3.11+. _TRY_NODE_TYPES = (ast.Try, ast.TryStar) if hasattr(ast, "TryStar") else (ast.Try,) diff --git a/python/private/pypi/dependency_resolver/dependency_resolver.py b/python/private/pypi/dependency_resolver/dependency_resolver.py index 34f2fb1e5a..2b24625c33 100644 --- a/python/private/pypi/dependency_resolver/dependency_resolver.py +++ b/python/private/pypi/dependency_resolver/dependency_resolver.py @@ -14,13 +14,14 @@ "Set defaults for the pip-compile command to run it under Bazel" +from __future__ import annotations + import atexit import functools import os import shutil import sys from pathlib import Path -from typing import List, Optional, Tuple import click import piptools.writer as piptools_writer @@ -32,7 +33,7 @@ # Replace the os.replace function with shutil.copy to work around os.replace not being able to # replace or move files across filesystems. -os.replace = shutil.copy +os.replace = shutil.copy # pyrefly: ignore[bad-assignment] # Next, we override the annotation_style_split and annotation_style_line functions to replace the # backslashes in the paths with forward slashes. This is so that we can have the same requirements @@ -91,13 +92,13 @@ def _locate(bazel_runfiles, file): @click.option("--requirements-windows") @click.argument("extra_args", nargs=-1, type=click.UNPROCESSED) def main( - srcs: Tuple[str, ...], + srcs: tuple[str, ...], requirements_txt: str, target_label_prefix: str, - requirements_linux: Optional[str], - requirements_darwin: Optional[str], - requirements_windows: Optional[str], - extra_args: Tuple[str, ...], + requirements_linux: str | None, + requirements_darwin: str | None, + requirements_windows: str | None, + extra_args: tuple[str, ...], ) -> None: bazel_runfiles = runfiles.Create() @@ -137,6 +138,7 @@ def main( os.environ["LANG"] = "C.UTF-8" argv = [] + requirements_out = requirements_file_relative UPDATE = True # Detect if we are running under `bazel test`. @@ -172,9 +174,7 @@ def main( os.environ["CUSTOM_COMPILE_COMMAND"] = update_command os.environ["PIP_CONFIG_FILE"] = os.getenv("PIP_CONFIG_FILE") or os.devnull - argv.append( - f"--output-file={requirements_file_relative if UPDATE else requirements_out}" - ) + argv.append(f"--output-file={requirements_out}") argv.extend( (src_relative if Path(src_relative).exists() else resolved_src) for src_relative, resolved_src in zip(srcs_relative, resolved_srcs) @@ -230,9 +230,9 @@ def main( def run_pip_compile( - args: List[str], + args: list[str], *, - srcs_relative: List[str], + srcs_relative: list[str], verbose_command: str, ) -> None: try: diff --git a/python/private/pypi/whl_installer/arguments.py b/python/private/pypi/whl_installer/arguments.py index 8471c94ffe..e6f5989c5d 100644 --- a/python/private/pypi/whl_installer/arguments.py +++ b/python/private/pypi/whl_installer/arguments.py @@ -14,7 +14,7 @@ import argparse import json -from typing import Any, Dict, Set +from typing import Any def parser(**kwargs: Any) -> argparse.ArgumentParser: @@ -57,7 +57,7 @@ def parser(**kwargs: Any) -> argparse.ArgumentParser: return parser -def deserialize_structured_args(args: Dict[str, str]) -> Dict: +def deserialize_structured_args(args: dict[str, Any]) -> dict[str, Any]: """Deserialize structured arguments passed from the starlark rules. Args: @@ -72,7 +72,7 @@ def deserialize_structured_args(args: Dict[str, str]) -> Dict: return args -def get_platforms(args: argparse.Namespace) -> Set: +def get_platforms(args: argparse.Namespace) -> set: """Aggregate platforms into a single set. Args: diff --git a/python/private/repl_template.py b/python/private/repl_template.py index dd8beb9784..8a6a62ca1a 100644 --- a/python/private/repl_template.py +++ b/python/private/repl_template.py @@ -35,9 +35,10 @@ def start_repl(): compiled_code = compile(source_code, filename=startup_file, mode="exec") eval(compiled_code, new_globals) - bazel_runfiles = runfiles.Create() + bazel_runfiles = runfiles.CreateOrRaise() + stub_path = bazel_runfiles.root() / STUB_PATH runpy.run_path( - bazel_runfiles.Rlocation(STUB_PATH), + str(stub_path), init_globals=new_globals, run_name="__main__", ) diff --git a/python/runfiles/BUILD.bazel b/python/runfiles/BUILD.bazel index 4e119eddbe..73663472dc 100644 --- a/python/runfiles/BUILD.bazel +++ b/python/runfiles/BUILD.bazel @@ -40,7 +40,6 @@ py_library( # to the --experimental_python_import_all_repositories setting. "../..", ], - tags = ["pyrefly"], visibility = ["//visibility:public"], ) diff --git a/python/runfiles/runfiles.py b/python/runfiles/runfiles.py index 02fceb3020..af87b54437 100644 --- a/python/runfiles/runfiles.py +++ b/python/runfiles/runfiles.py @@ -23,20 +23,23 @@ ::: """ +from __future__ import annotations + import inspect import os import pathlib import posixpath import sys from collections import defaultdict -from typing import Dict, Generator, Optional, Tuple, Union +from collections.abc import Generator +from typing import cast if sys.version_info >= (3, 11): from typing import Self elif sys.version_info >= (3, 10): from typing import TypeAlias - Self: TypeAlias = "Path" # type: ignore + Self: TypeAlias = "Path" # pyrefly: ignore[invalid-type-form] else: from typing import Any as Self @@ -50,8 +53,8 @@ class _RepositoryMapping: def __init__( self, - exact_mappings: Dict[Tuple[str, str], str], - prefixed_mappings: Dict[Tuple[str, str], str], + exact_mappings: dict[tuple[str, str], str], + prefixed_mappings: dict[tuple[str, str], str], ) -> None: """Initialize repository mapping with exact and prefixed mappings. @@ -72,7 +75,7 @@ def __init__( ) @staticmethod - def create_from_file(repo_mapping_path: Optional[str]) -> "_RepositoryMapping": + def create_from_file(repo_mapping_path: str | None) -> _RepositoryMapping: """Create RepositoryMapping from a repository mapping manifest file. Args: @@ -107,7 +110,7 @@ def create_from_file(repo_mapping_path: Optional[str]) -> "_RepositoryMapping": return _RepositoryMapping(exact_mappings, prefixed_mappings) - def lookup(self, source_repo: Optional[str], target_apparent: str) -> Optional[str]: + def lookup(self, source_repo: str | None, target_apparent: str) -> str | None: """Look up repository mapping for the given source and target. This handles both exact mappings and prefix-based mappings introduced by the @@ -161,31 +164,29 @@ class Path(pathlib.Path): # Mypy isn't smart enough to realize `self` in the methods # refers to our Path class instead of pathlib.Path - _runfiles: Optional["Runfiles"] - _source_repo: Optional[str] + _runfiles: Runfiles | None + _source_repo: str | None # For Python < 3.12 compatibility when subclassing Path directly _flavour = getattr(type(pathlib.Path()), "_flavour", None) def __new__( cls, - *args: Union[str, os.PathLike], - runfiles: Optional["Runfiles"] = None, - source_repo: Optional[str] = None, + *args: str | os.PathLike, + runfiles: Runfiles | None = None, + source_repo: str | None = None, ) -> Self: """Private constructor. Use Runfiles.root() to create instances.""" - obj = super().__new__(cls, *args) - # Type checkers might complain about adding attributes to Path, - # but this is standard for pathlib subclasses. - obj._runfiles = runfiles # type: ignore - obj._source_repo = source_repo # type: ignore - return obj + obj = cast("Path", super().__new__(cls, *args)) + obj._runfiles = runfiles + obj._source_repo = source_repo + return cast(Self, obj) def __init__( self, - *args: Union[str, os.PathLike], - runfiles: Optional["Runfiles"] = None, - source_repo: Optional[str] = None, + *args: str | os.PathLike, + runfiles: Runfiles | None = None, + source_repo: str | None = None, ) -> None: # In Python 3.12+, pathlib was refactored and Path.__init__ now accepts # *args. Prior to 3.12, Path did not define __init__, so @@ -218,7 +219,7 @@ def absolute(self) -> Self: ) # override - def with_segments(self, *pathsegments: Union[str, os.PathLike]) -> Self: + def with_segments(self, *pathsegments: str | os.PathLike) -> Self: """Used by Python 3.12+ pathlib to create new path objects.""" return type(self)( *pathsegments, @@ -228,15 +229,17 @@ def with_segments(self, *pathsegments: Union[str, os.PathLike]) -> Self: # For Python < 3.12 # override - def _make_child(self, args: Tuple[str, ...]) -> Self: - obj = super()._make_child(args) # type: ignore - obj._runfiles = self._runfiles # type: ignore - obj._source_repo = self._source_repo # type: ignore - return obj + def _make_child(self, args: tuple[str, ...]) -> Self: + # _make_child is an internal CPython method in Python < 3.12 omitted from + # typeshed stubs. We ignore [misc] for mypy and [missing-attribute] for pyrefly. + obj = cast("Path", super()._make_child(args)) # type: ignore[misc] # pyrefly: ignore[missing-attribute] + obj._runfiles = self._runfiles + obj._source_repo = self._source_repo + return cast(Self, obj) # override @property - def parents(self) -> Tuple[Self, ...]: + def parents(self) -> tuple[Self, ...]: return tuple( type(self)( p, @@ -322,14 +325,16 @@ def is_fifo(self) -> bool: def is_socket(self) -> bool: return self._as_path().is_socket() + # Path.open in pathlib has multiple overloads in typeshed. We use a + # simplified delegation signature here. # override def open( # pyrefly: ignore[bad-override] self, mode: str = "r", buffering: int = -1, - encoding: Optional[str] = None, - errors: Optional[str] = None, - newline: Optional[str] = None, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, ): return self._as_path().open( mode=mode, @@ -344,9 +349,7 @@ def read_bytes(self) -> bytes: return self._as_path().read_bytes() # override - def read_text( - self, encoding: Optional[str] = None, errors: Optional[str] = None - ) -> str: + def read_text(self, encoding: str | None = None, errors: str | None = None) -> str: return self._as_path().read_text(encoding=encoding, errors=errors) # override @@ -371,23 +374,25 @@ def __repr__(self) -> str: return "runfiles.Path({!r})".format(self.runfile_path) def __str__(self) -> str: + assert self._runfiles is not None # type assert path_posix = super().__str__().replace("\\", "/") if not path_posix or path_posix == ".": # pylint: disable=protected-access - return self._runfiles._python_runfiles_root # type: ignore - resolved = self._runfiles.Rlocation(path_posix, source_repo=self._source_repo) # type: ignore + return self._runfiles._python_runfiles_root # type: ignore[attr-defined] # pyrefly: ignore[missing-attribute] + resolved = self._runfiles.Rlocation(path_posix, source_repo=self._source_repo) if resolved is not None: return resolved # pylint: disable=protected-access - return posixpath.join(self._runfiles._python_runfiles_root, path_posix) # type: ignore + return posixpath.join(self._runfiles._python_runfiles_root, path_posix) # type: ignore[attr-defined] # pyrefly: ignore[missing-attribute] def __fspath__(self) -> str: return str(self) - def runfiles_root(self) -> Self: + def runfiles_root(self) -> "Path": """Returns a Path object representing the runfiles root.""" - return self._runfiles.root(source_repo=self._source_repo) # type: ignore + assert self._runfiles is not None # type assert + return self._runfiles.root(source_repo=self._source_repo) class _ManifestBased: @@ -401,7 +406,7 @@ def __init__(self, path: str) -> None: self._path = path self._runfiles = _ManifestBased._LoadRunfiles(path) - def RlocationChecked(self, path: str) -> Optional[str]: + def RlocationChecked(self, path: str) -> str | None: """Returns the runtime path of a runfile.""" exact_match = self._runfiles.get(path) if exact_match: @@ -420,7 +425,7 @@ def RlocationChecked(self, path: str) -> Optional[str]: return prefix_match + "/" + path[prefix_end + 1 :] @staticmethod - def _LoadRunfiles(path: str) -> Dict[str, str]: + def _LoadRunfiles(path: str) -> dict[str, str]: """Loads the runfiles manifest.""" result = {} with open(path, "r", encoding="utf-8", newline="\n") as f: @@ -452,7 +457,7 @@ def _GetRunfilesDir(self) -> str: return self._path[: -len("_manifest")] return "" - def EnvVars(self) -> Dict[str, str]: + def EnvVars(self) -> dict[str, str]: directory = self._GetRunfilesDir() return { "RUNFILES_MANIFEST_FILE": self._path, @@ -482,7 +487,7 @@ def RlocationChecked(self, path: str) -> str: def _GetRunfilesDir(self) -> str: return self._runfiles_root - def EnvVars(self) -> Dict[str, str]: + def EnvVars(self) -> dict[str, str]: return { "RUNFILES_DIR": self._runfiles_root, # TODO(laszlocsomor): remove JAVA_RUNFILES once the Java launcher can @@ -497,14 +502,14 @@ class Runfiles: Runfiles are data-dependencies of Bazel-built binaries and tests. """ - def __init__(self, strategy: Union[_ManifestBased, _DirectoryBased]) -> None: + def __init__(self, strategy: _ManifestBased | _DirectoryBased) -> None: self._strategy = strategy self._python_runfiles_root = strategy._GetRunfilesDir() self._repo_mapping = _RepositoryMapping.create_from_file( strategy.RlocationChecked("_repo_mapping") ) - def root(self, source_repo: Optional[str] = None) -> Path: + def root(self, source_repo: str | None = None) -> Path: """Returns a Path object representing the runfiles root. The repository mapping used by the returned Path object is that of the @@ -514,7 +519,7 @@ def root(self, source_repo: Optional[str] = None) -> Path: source_repo = self.CurrentRepository(frame=2) return Path(runfiles=self, source_repo=source_repo) - def Rlocation(self, path: str, source_repo: Optional[str] = None) -> Optional[str]: + def Rlocation(self, path: str, source_repo: str | None = None) -> str | None: """Returns the runtime path of a runfile. Runfiles are data-dependencies of Bazel-built binaries and tests. @@ -591,7 +596,7 @@ def Rlocation(self, path: str, source_repo: Optional[str] = None) -> Optional[st # we're not using Bzlmod return self._strategy.RlocationChecked(path) - def EnvVars(self) -> Dict[str, str]: + def EnvVars(self) -> dict[str, str]: """Returns environment variables for subprocesses. The caller should set the returned key-value pairs in the environment of @@ -693,7 +698,7 @@ def CreateDirectoryBased(runfiles_dir_path: str) -> "Runfiles": # TODO: Update return type to Self when 3.11 is the min version # https://peps.python.org/pep-0673/ @staticmethod - def Create(env: Optional[Dict[str, str]] = None) -> Optional["Runfiles"]: + def Create(env: dict[str, str] | None = None) -> Runfiles | None: """Returns a new `Runfiles` instance. The returned object is either: @@ -731,7 +736,7 @@ def Create(env: Optional[Dict[str, str]] = None) -> Optional["Runfiles"]: # TODO: Update return type to Self when 3.11 is the min version # https://peps.python.org/pep-0673/ @staticmethod - def CreateOrRaise(env: Optional[Dict[str, str]] = None) -> "Runfiles": + def CreateOrRaise(env: dict[str, str] | None = None) -> Runfiles: """Returns a new `Runfiles` instance, or raises an error. The returned object is either: @@ -781,11 +786,11 @@ def CreateDirectoryBased(runfiles_dir_path: str) -> Runfiles: return Runfiles.CreateDirectoryBased(runfiles_dir_path) -def Create(env: Optional[Dict[str, str]] = None) -> Optional[Runfiles]: +def Create(env: dict[str, str] | None = None) -> Runfiles | None: return Runfiles.Create(env) -def CreateOrRaise(env: Optional[Dict[str, str]] = None) -> Runfiles: +def CreateOrRaise(env: dict[str, str] | None = None) -> Runfiles: """Refer to `Runfiles.CreateOrRaise`. :::{versionadded} VERSION_NEXT_FEATURE diff --git a/sphinxdocs/.bazelrc b/sphinxdocs/.bazelrc index ce4d782113..6989c2656a 100644 --- a/sphinxdocs/.bazelrc +++ b/sphinxdocs/.bazelrc @@ -31,3 +31,7 @@ build --lockfile_mode=update common:fast-tests --build_tests_only=true common:fast-tests --build_tag_filters=-large,-enormous,-integration-test common:fast-tests --test_tag_filters=-large,-enormous,-integration-test + +build --config=pyrefly +build:pyrefly --aspects=//tests/support/pyrefly:pyrefly.bzl%pyrefly_aspect +build:pyrefly --output_groups=+pyrefly diff --git a/sphinxdocs/MODULE.bazel b/sphinxdocs/MODULE.bazel index 51de5e94f9..c1f06ccb50 100644 --- a/sphinxdocs/MODULE.bazel +++ b/sphinxdocs/MODULE.bazel @@ -21,7 +21,7 @@ dev_pip.parse( requirements_lock = "//dev:requirements.txt", uv_lock = "//dev:uv.lock", ) -use_repo(dev_pip, "dev_pip") +use_repo(dev_pip, "dev_pip", "pypi") bazel_dep(name = "rules_bazel_integration_test", version = "0.37.1", dev_dependency = True) @@ -40,3 +40,18 @@ use_repo( "bazel_binaries_bazelisk", "build_bazel_bazel_self", ) + +bazel_dep(name = "rules_pyrefly", version = "0.1.0", dev_dependency = True) + +pyrefly = use_extension( + "@rules_pyrefly//pyrefly:extensions.bzl", + "pyrefly", + dev_dependency = True, +) +pyrefly.toolchain(version = "1.2.0") +use_repo(pyrefly, "pyrefly_toolchains") + +register_toolchains( + "@pyrefly_toolchains//:all", + dev_dependency = True, +) diff --git a/sphinxdocs/integration_tests/runner.py b/sphinxdocs/integration_tests/runner.py index cab9730bb8..c7cc753e93 100644 --- a/sphinxdocs/integration_tests/runner.py +++ b/sphinxdocs/integration_tests/runner.py @@ -72,19 +72,19 @@ def setUp(self): } def run_bazel(self, *args: str, check: bool = True) -> ExecuteResult: - args = [str(self.bazel), *args] + cmd_args = [str(self.bazel), *args] env = self.bazel_env - _logger.info("executing: %s", shlex.join(args)) + _logger.info("executing: %s", shlex.join(cmd_args)) cwd = self.repo_root proc_result = subprocess.run( - args=args, + args=cmd_args, text=True, capture_output=True, cwd=cwd, env=env, check=False, ) - exec_result = ExecuteResult(args, env, cwd, proc_result) + exec_result = ExecuteResult(cmd_args, env, cwd, proc_result) if check and exec_result.exit_code: raise ExecuteError(exec_result) else: diff --git a/sphinxdocs/sphinxdocs/private/proto_to_markdown.py b/sphinxdocs/sphinxdocs/private/proto_to_markdown.py index 05278a5c02..aeecc359f7 100644 --- a/sphinxdocs/sphinxdocs/private/proto_to_markdown.py +++ b/sphinxdocs/sphinxdocs/private/proto_to_markdown.py @@ -12,13 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations + import argparse import itertools import pathlib import sys -from typing import Callable, TextIO, TypeVar +from collections.abc import Callable, Iterator, Sequence +from typing import TextIO, TypeVar -from stardoc.proto import stardoc_output_pb2 +from stardoc.proto import ( # pyrefly: ignore[missing-import] + stardoc_output_pb2, +) _AttributeType = stardoc_output_pb2.AttributeType @@ -73,7 +78,7 @@ def _join_csv_and(values: list[str]) -> str: return ", ".join(values) -def _position_iter(values: list[_T]) -> tuple[bool, bool, _T]: +def _position_iter(values: Sequence[_T]) -> Iterator[tuple[bool, bool, _T]]: for i, value in enumerate(values): yield i == 0, i == len(values) - 1, value @@ -438,7 +443,9 @@ def _render_provider(self, provider: stardoc_output_pb2.ProviderInfo): self._write(":::::\n") self._write("::::::\n") - def _render_attributes(self, attributes: list[stardoc_output_pb2.AttributeInfo]): + def _render_attributes( + self, attributes: Sequence[stardoc_output_pb2.AttributeInfo] + ): for attr in attributes: attr_type = self._rule_attr_type_string(attr) self._write(f":attr {attr.name}:\n") @@ -491,10 +498,10 @@ def _render_attributes(self, attributes: list[stardoc_output_pb2.AttributeInfo]) def _render_signature( self, name: str, - parameters: list[_T], + parameters: Sequence[_T], *, - get_name: Callable[_T, str], - get_default: Callable[_T, str] = lambda v: None, + get_name: Callable[[_T], str], + get_default: Callable[[_T], str | None] = lambda v: None, ): self._write(name, "(") for _, is_last, param in _position_iter(parameters): diff --git a/sphinxdocs/sphinxdocs/private/sphinx_build.py b/sphinxdocs/sphinxdocs/private/sphinx_build.py index 52a334d9b9..f20fbd676a 100644 --- a/sphinxdocs/sphinxdocs/private/sphinx_build.py +++ b/sphinxdocs/sphinxdocs/private/sphinx_build.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import concurrent.futures import contextlib import io @@ -10,13 +12,55 @@ import sys import threading import traceback -import typing +import types +from typing import TextIO, TypedDict + +import sphinx.application # pyrefly: ignore[missing-import] +from sphinx.cmd.build import main # pyrefly: ignore[missing-import] + + +class WorkRequestInput(TypedDict, total=False): + """Input file with digest for a Bazel persistent worker WorkRequest. + + See https://github.com/bazelbuild/bazel/blob/master/src/main/protobuf/worker_protocol.proto (Input message). + """ + + path: str + digest: str + + +class WorkRequest(TypedDict, total=False): + """Bazel persistent worker WorkRequest protocol structure. + + See https://github.com/bazelbuild/bazel/blob/master/src/main/protobuf/worker_protocol.proto (WorkRequest message). + """ -import sphinx.application -from sphinx.cmd.build import main + id: int + requestId: int + arguments: list[str] + inputs: list[WorkRequestInput] + cancel: bool -WorkRequest = object -WorkResponse = object + +class WorkResponse(TypedDict, total=False): + """Bazel persistent worker WorkResponse protocol structure. + + See https://github.com/bazelbuild/bazel/blob/master/src/main/protobuf/worker_protocol.proto (WorkResponse message). + """ + + id: int + requestId: int + exitCode: int + output: str + wasCancelled: bool + + +class RequestInfo(TypedDict, total=False): + """JSON structure written for the Sphinx extension with worker request metadata.""" + + exec_root: str + inputs: list[WorkRequestInput] + changed_sources: list[str] class SphinxMainError(Exception): @@ -36,7 +80,7 @@ def __init__(self, message, exit_code): class DirectorySyncerError(Exception): """Raised when one or more errors occur during directory synchronization.""" - def __init__(self, errors: typing.List[BaseException]): + def __init__(self, errors: list[BaseException]): self.errors = errors message = f"Encountered {len(errors)} error(s) during sync:\n" + "\n".join( f" - {e}" for e in errors @@ -57,17 +101,17 @@ def __init__( self, srcdir: pathlib.Path, destdir: pathlib.Path, - max_workers: typing.Optional[int] = None, + max_workers: int | None = None, ): self._srcdir = srcdir self._destdir = destdir self._max_workers = max_workers or min(32, (os.cpu_count() or 4) + 4) - self._current_shas: typing.Dict[str, str] = {} + self._current_shas: dict[str, str] = {} self._lock = threading.Lock() self._finished_cond = threading.Condition(self._lock) self._remaining = 0 - self._errors: typing.List[BaseException] = [] - self._executor: typing.Optional[concurrent.futures.ThreadPoolExecutor] = None + self._errors: list[BaseException] = [] + self._executor: concurrent.futures.ThreadPoolExecutor | None = None def _reset_state(self) -> None: with self._lock: @@ -84,6 +128,7 @@ def _wait_for_completion(self) -> None: def _submit_task(self, fn, *args) -> None: with self._lock: self._remaining += 1 + assert self._executor is not None future = self._executor.submit(fn, *args) future.add_done_callback(self._handle_task_done) @@ -118,7 +163,7 @@ def copytree(self) -> None: self._submit_task(self._copy_dir, self._srcdir, self._destdir) self._wait_for_completion() - def sync(self, entries: typing.Dict[str, str]) -> None: + def sync(self, entries: dict[str, str]) -> None: """Synchronizes destdir to match entries {relative_path: sha} concurrently.""" self._reset_state() @@ -198,9 +243,7 @@ def _copy_dir(self, src: pathlib.Path, dest: pathlib.Path) -> None: class Worker: """A Bazel persistent worker for Sphinx builds.""" - def __init__( - self, instream: "typing.TextIO", outstream: "typing.TextIO", exec_root: str - ): + def __init__(self, instream: TextIO, outstream: TextIO, exec_root: str): # NOTE: Sphinx performs its own logging re-configuration, so any # logging config we do isn't respected by Sphinx. Controlling where # stdout and stderr goes are the main mechanisms. Recall that @@ -219,7 +262,7 @@ def __init__( # dict[str srcdir, dict[str path, str digest]] self._digests = {} - self._syncers: typing.Dict[pathlib.Path, DirectorySyncer] = {} + self._syncers: dict[pathlib.Path, DirectorySyncer] = {} # Internal output directories the worker gives to Sphinx that need # to be cleaned up upon exit. @@ -266,11 +309,12 @@ def run(self) -> None: ) except Exception: logger.exception("Unhandled error: request=%s", request) + request_id = request.get("requestId", 0) if request else 0 + req_id_str = request.get("id") if request else "unknown" output = ( - f"Unhandled error:\nRequest id: {request.get('id')}\n" + f"Unhandled error:\nRequest id: {req_id_str}\n" + traceback.format_exc() ) - request_id = 0 if not request else request.get("requestId", 0) self._send_response( { "exitCode": 3, @@ -281,17 +325,17 @@ def run(self) -> None: finally: logger.info("Worker shutting down") - def _get_next_request(self) -> "object | None": + def _get_next_request(self) -> WorkRequest | None: line = self._instream.readline() if not line: return None return json.loads(line) - def _send_response(self, response: "WorkResponse") -> None: + def _send_response(self, response: WorkResponse) -> None: self._outstream.write(json.dumps(response) + "\n") self._outstream.flush() - def _prepare_sphinx(self, request): + def _prepare_sphinx(self, request: WorkRequest): sphinx_args = request["arguments"] srcdir = pathlib.Path(sphinx_args[0]) destdir = pathlib.Path(f"{srcdir}.worker-in.d") @@ -300,9 +344,12 @@ def _prepare_sphinx(self, request): current_digests = self._digests.setdefault(str(srcdir), {}) is_first_request = not current_digests changed_paths = [] - request_info = {"exec_root": self._exec_root, "inputs": request["inputs"]} + request_info: RequestInfo = { + "exec_root": self._exec_root, + "inputs": request.get("inputs", []), + } srcdir_prefix = str(srcdir) + "/" - for entry in request["inputs"]: + for entry in request.get("inputs", []): path = entry["path"] # In persistent worker mode, request["inputs"] includes action-level # tools (e.g. sphinx-build, sphinx_build.py) and params files that @@ -322,7 +369,7 @@ def _prepare_sphinx(self, request): changed_paths.append(path) self._digests[str(srcdir)] = incoming_digests - self._extension.changed_paths = changed_paths + self._extension.changed_paths = set(changed_paths) request_info["changed_sources"] = changed_paths bazel_outdir = sphinx_args[1] @@ -365,7 +412,7 @@ def _redirect_streams(self): with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): yield stdout, stderr - def _process_request(self, request: "WorkRequest") -> "WorkResponse | None": + def _process_request(self, request: WorkRequest) -> WorkResponse | None: logger.info("Request: %s", json.dumps(request, sort_keys=True, indent=2)) if request.get("cancel"): return None @@ -446,14 +493,13 @@ def _process_request(self, request: "WorkRequest") -> "WorkResponse | None": return response -class BazelWorkerExtension: +class BazelWorkerExtension(types.ModuleType): """A Sphinx extension implemented as a class acting like a module.""" - def __init__(self): - # Make it look like a Module object - self.__name__ = _WORKER_SPHINX_EXT_MODULE_NAME + def __init__(self, name: str = _WORKER_SPHINX_EXT_MODULE_NAME): + super().__init__(name) # set[str] of src-dir relative path names - self.changed_paths = set() + self.changed_paths: set[str] = set() def setup(self, app): app.add_config_value(_REQUEST_INFO_CONFIG_NAME, "", "") diff --git a/sphinxdocs/sphinxdocs/src/sphinx_bzl/bzl.py b/sphinxdocs/sphinxdocs/src/sphinx_bzl/bzl.py index c115737ba5..9cef0cce67 100644 --- a/sphinxdocs/sphinxdocs/src/sphinx_bzl/bzl.py +++ b/sphinxdocs/sphinxdocs/src/sphinx_bzl/bzl.py @@ -13,17 +13,23 @@ # limitations under the License. """Sphinx extension for documenting Bazel/Starlark objects.""" +from __future__ import annotations + import ast import collections import enum import os -import typing -from collections.abc import Collection -from typing import Callable, Iterable, TypeVar +from collections.abc import Callable, Collection, Iterable, Iterator, Set +from typing import Any, TypeVar, cast -from docutils import nodes as docutils_nodes -from docutils.parsers.rst import directives as docutils_directives, states -from sphinx import ( +from docutils import ( # pyrefly: ignore[missing-source-for-stubs] + nodes as docutils_nodes, +) +from docutils.parsers.rst import ( # pyrefly: ignore[missing-source-for-stubs] + directives as docutils_directives, + states, +) +from sphinx import ( # pyrefly: ignore[missing-import] addnodes, builders, directives as sphinx_directives, @@ -31,9 +37,11 @@ environment, roles, ) -from sphinx.highlighting import lexer_classes -from sphinx.locale import _ -from sphinx.util import ( +from sphinx.highlighting import ( # pyrefly: ignore[missing-import] + lexer_classes, +) +from sphinx.locale import _ # pyrefly: ignore[missing-import] +from sphinx.util import ( # pyrefly: ignore[missing-import] docfields, docutils as sphinx_docutils, inspect, @@ -68,7 +76,7 @@ def _log_debug(message, *args): _logger.debug("%s" + message, _LOG_PREFIX, *args) -def _position_iter(values: Collection[_T]) -> tuple[bool, bool, _T]: +def _position_iter(values: Collection[_T]) -> Iterator[tuple[bool, bool, _T]]: last_i = len(values) - 1 for i, value in enumerate(values): yield i == 0, i == last_i, value @@ -133,9 +141,9 @@ def _index_node_tuple( entry_type: str, entry_name: str, target: str, - main: typing.Union[str, None] = None, - category_key: typing.Union[str, None] = None, -) -> tuple[str, str, str, typing.Union[str, None], typing.Union[str, None]]: + main: str | None = None, + category_key: str | None = None, +) -> tuple[str, str, str, str | None, str | None]: # For this tuple definition, see: # https://www.sphinx-doc.org/en/master/extdev/nodes.html#sphinx.addnodes.index # For the definition of entry_type, see: @@ -157,8 +165,8 @@ def __init__( *, repo: str, label: str, - namespace: str = None, - symbol: str = None, + namespace: str | None = None, + symbol: str | None = None, ): """Creates an instance. @@ -197,7 +205,11 @@ def __init__( @classmethod def from_env( - cls, env: environment.BuildEnvironment, *, symbol: str = None, label: str = None + cls, + env: environment.BuildEnvironment, + *, + symbol: str | None = None, + label: str | None = None, ) -> "_BzlObjectId": label = label or env.ref_context["bzl:file"] if symbol: @@ -250,7 +262,7 @@ class _TypeExprParser(ast.NodeVisitor): def __init__(self, make_xref: Callable[[str], docutils_nodes.Node]): self.root_node = addnodes.desc_inline("bzl", classes=["type-expr"]) self.make_xref = make_xref - self._doc_node_stack = [self.root_node] + self._doc_node_stack: list[docutils_nodes.Element] = [self.root_node] @classmethod def xrefs_from_type_expr( @@ -266,7 +278,7 @@ def xrefs_from_type_expr( def _append(self, node: docutils_nodes.Node): self._doc_node_stack[-1] += node - def _append_and_push(self, node: docutils_nodes.Node): + def _append_and_push(self, node: docutils_nodes.Element): self._append(node) self._doc_node_stack.append(node) @@ -339,17 +351,18 @@ def generic_visit(self, node): class _BzlXrefField(docfields.Field): """Abstract base class to create cross references for fields.""" + # docfields.Field lacks type stubs, so @override triggers bad-override. @override - def make_xrefs( + def make_xrefs( # pyrefly: ignore[bad-override] self, rolename: str, domain: str, target: str, innernode: type[sphinx_typing.TextlikeNode] = addnodes.literal_emphasis, - contnode: typing.Union[docutils_nodes.Node, None] = None, - env: typing.Union[environment.BuildEnvironment, None] = None, - inliner: typing.Union[states.Inliner, None] = None, - location: typing.Union[docutils_nodes.Element, None] = None, + contnode: docutils_nodes.Node | None = None, + env: environment.BuildEnvironment | None = None, + inliner: states.Inliner | None = None, + location: docutils_nodes.Node | tuple[str, int] | None = None, ) -> list[docutils_nodes.Node]: if rolename in ("arg", "attr"): return self._make_xrefs_for_arg_attr( @@ -366,11 +379,12 @@ def _make_xrefs_for_arg_attr( domain: str, arg_name: str, innernode: type[sphinx_typing.TextlikeNode] = addnodes.literal_emphasis, - contnode: typing.Union[docutils_nodes.Node, None] = None, - env: typing.Union[environment.BuildEnvironment, None] = None, - inliner: typing.Union[states.Inliner, None] = None, - location: typing.Union[docutils_nodes.Element, None] = None, + contnode: docutils_nodes.Node | None = None, + env: environment.BuildEnvironment | None = None, + inliner: states.Inliner | None = None, + location: docutils_nodes.Node | tuple[str, int] | None = None, ) -> list[docutils_nodes.Node]: + assert env is not None bzl_file = env.ref_context["bzl:file"] anchor_prefix = ".".join(env.ref_context["bzl:doc_id_stack"]) if not anchor_prefix: @@ -381,7 +395,8 @@ def _make_xrefs_for_arg_attr( anchor_id = f"{anchor_prefix}.{arg_name}" full_id = _full_id_from_env(env, [arg_name]) - env.get_domain(domain).add_object( + bzl_domain = cast(_BzlDomain, env.get_domain(domain)) + bzl_domain.add_object( _ObjectEntry( full_id=full_id, display_name=arg_name, @@ -454,10 +469,10 @@ def make_field( self, types: dict[str, list[docutils_nodes.Node]], domain: str, - item: tuple, - env: environment.BuildEnvironment = None, - inliner: typing.Union[states.Inliner, None] = None, - location: typing.Union[docutils_nodes.Element, None] = None, + item: tuple[str, list[docutils_nodes.Node]], + env: environment.BuildEnvironment | None = None, + inliner: states.Inliner | None = None, + location: docutils_nodes.Element | None = None, ) -> docutils_nodes.field: field_text = item[1][0].astext() parts = [p.strip() for p in field_text.split(",")] @@ -498,8 +513,9 @@ class _BzlCurrentFile(sphinx_docutils.SphinxDirective): required_arguments = 1 final_argument_whitespace = False + # SphinxDirective lacks type stubs, so @override triggers bad-override. @override - def run(self) -> list[docutils_nodes.Node]: + def run(self) -> list[docutils_nodes.Node]: # pyrefly: ignore[bad-override] label = self.arguments[0].strip() repo, slashes, file_label = label.partition("//") file_label = slashes + file_label @@ -528,7 +544,8 @@ def run(self) -> list[docutils_nodes.Node]: index_description = f"File {label}" absolute_label = repo + label - self.env.get_domain("bzl").add_object( + bzl_domain = cast(_BzlDomain, self.env.get_domain("bzl")) + bzl_domain.add_object( _ObjectEntry( full_id=absolute_label, display_name=absolute_label, @@ -602,18 +619,22 @@ class _BzlObject(sphinx_directives.ObjectDescription[_BzlObjectId]): "origin-key": docutils_directives.unchanged, } + # ObjectDescription lacks type stubs, so @override triggers bad-override. @override - def before_content(self) -> None: + def before_content(self) -> None: # pyrefly: ignore[bad-override] symbol_name = self.names[-1].symbol if symbol_name: self.env.ref_context["bzl:object_id_stack"].append(symbol_name) self.env.ref_context["bzl:doc_id_stack"].append(symbol_name) + # ObjectDescription lacks type stubs, so @override triggers bad-override. @override - def transform_content(self, content_node: addnodes.desc_content) -> None: + def transform_content( # pyrefly: ignore[bad-override] + self, contentnode: addnodes.desc_content + ) -> None: def first_child_with_class_name( root, class_name - ) -> typing.Union[None, docutils_nodes.Element]: + ) -> docutils_nodes.Element | None: matches = root.findall( lambda node: ( isinstance(node, docutils_nodes.Element) @@ -632,7 +653,7 @@ def match_arg_field_name(node): # fmt: on # Move the spans for the arg type and default value to be first. - arg_name_fields = list(content_node.findall(match_arg_field_name)) + arg_name_fields = list(contentnode.findall(match_arg_field_name)) for arg_name_field in arg_name_fields: arg_body_field = arg_name_field.next_node(descend=False, siblings=True) # arg_type_node = first_child_with_class_name(arg_body_field, "arg-type-span") @@ -647,10 +668,12 @@ def match_arg_field_name(node): # doc text) if arg_default_node: + assert arg_default_node.parent is not None arg_default_node.parent.remove(arg_default_node) arg_body_field.insert(0, arg_default_node) if arg_type_node: + assert arg_type_node.parent is not None arg_type_node.parent.remove(arg_type_node) decorated_arg_type_node = docutils_nodes.inline( "", @@ -663,21 +686,23 @@ def match_arg_field_name(node): # arg_body_field.insert(0, arg_type_node) arg_body_field.insert(0, decorated_arg_type_node) + # ObjectDescription lacks type stubs, so @override triggers bad-override. @override - def after_content(self) -> None: + def after_content(self) -> None: # pyrefly: ignore[bad-override] if self.names[-1].symbol: self.env.ref_context["bzl:object_id_stack"].pop() self.env.ref_context["bzl:doc_id_stack"].pop() # docs on how to build signatures: # https://www.sphinx-doc.org/en/master/extdev/nodes.html#sphinx.addnodes.desc_signature + # ObjectDescription lacks type stubs, so @override triggers bad-override. @override - def handle_signature( - self, sig_text: str, sig_node: addnodes.desc_signature + def handle_signature( # pyrefly: ignore[bad-override] + self, sig: str, signode: addnodes.desc_signature ) -> _BzlObjectId: - self._signature_add_object_type(sig_node) + self._signature_add_object_type(signode) - relative_name, lparen, params_text = sig_text.partition("(") + relative_name, lparen, params_text = sig.partition("(") if lparen: params_text = lparen + params_text @@ -696,8 +721,8 @@ def handle_signature( if display_prefix: display_prefix = display_prefix + "." - sig_node += addnodes.desc_addname(display_prefix, display_prefix) - sig_node += addnodes.desc_name(base_symbol_name, base_symbol_name) + signode += addnodes.desc_addname(display_prefix, display_prefix) + signode += addnodes.desc_name(base_symbol_name, base_symbol_name) if type_expr := self.options.get("type"): @@ -718,7 +743,7 @@ def make_xref(name, title=None): addnodes.desc_sig_space(), _TypeExprParser.xrefs_from_type_expr(type_expr, make_xref), ) - sig_node += attr_annotation_node + signode += attr_annotation_node if params_text: try: @@ -728,7 +753,7 @@ def make_xref(name, title=None): # signature might not be valid syntax. Rather than fail, just # provide a plain-text description of the approximate signature. # See https://github.com/bazelbuild/stardoc/issues/225 - sig_node += addnodes.desc_parameterlist( + signode += addnodes.desc_parameterlist( # Offset by 1 to remove the surrounding parentheses params_text[1:-1], params_text[1:-1], @@ -764,14 +789,14 @@ def make_xref(name, title=None): support_smartquotes=False, ) paramlist_node += node - sig_node += paramlist_node + signode += paramlist_node if signature.return_annotation is not signature.empty: - sig_node += addnodes.desc_returns("", signature.return_annotation) + signode += addnodes.desc_returns("", signature.return_annotation) obj_id = _BzlObjectId.from_env(self.env, symbol=relative_name) - sig_node["bzl:object_id"] = obj_id.full_id + signode["bzl:object_id"] = obj_id.full_id return obj_id def _signature_add_object_type(self, sig_node: addnodes.desc_signature): @@ -779,27 +804,28 @@ def _signature_add_object_type(self, sig_node: addnodes.desc_signature): sig_node += addnodes.desc_annotation("", self._get_signature_object_type()) sig_node += addnodes.desc_sig_space() + # ObjectDescription lacks type stubs, so @override triggers bad-override. @override - def add_target_and_index( - self, obj_desc: _BzlObjectId, sig: str, sig_node: addnodes.desc_signature + def add_target_and_index( # pyrefly: ignore[bad-override] + self, name: _BzlObjectId, sig: str, signode: addnodes.desc_signature ) -> None: - super().add_target_and_index(obj_desc, sig, sig_node) - if obj_desc.symbol: - display_name = obj_desc.symbol - location = obj_desc.label - if obj_desc.namespace: - location += f"%{obj_desc.namespace}" + super().add_target_and_index(name, sig, signode) + if name.symbol: + display_name = name.symbol + location = name.label + if name.namespace: + location += f"%{name.namespace}" else: - display_name = obj_desc.target_name - location = obj_desc.package + display_name = name.target_name + location = name.package anchor_prefix = ".".join(self.env.ref_context["bzl:doc_id_stack"]) if anchor_prefix: - anchor_id = f"{anchor_prefix}.{obj_desc.doc_id}" + anchor_id = f"{anchor_prefix}.{name.doc_id}" else: - anchor_id = obj_desc.doc_id + anchor_id = name.doc_id - sig_node["ids"].append(anchor_id) + signode["ids"].append(anchor_id) object_type_display = self._get_object_type_display_name() index_description = f"{display_name} ({object_type_display} in {location})" @@ -812,7 +838,7 @@ def add_target_and_index( ) object_entry = _ObjectEntry( - full_id=obj_desc.full_id, + full_id=name.full_id, display_name=display_name, object_type=self.objtype, search_priority=1, @@ -838,29 +864,38 @@ def add_target_and_index( extra_alt_names = self._get_alt_names(object_entry) alt_names.extend(extra_alt_names) - self.env.get_domain(self.domain).add_object(object_entry, alt_names=alt_names) + domain = self._get_bzl_domain() + domain.add_object(object_entry, alt_names=alt_names) + + def _get_bzl_domain(self) -> _BzlDomain: + domain_name = self.domain or "bzl" + return cast(_BzlDomain, self.env.get_domain(domain_name)) def _get_additional_index_types(self): return [] + # ObjectDescription lacks type stubs, so @override triggers bad-override. @override - def _object_hierarchy_parts( + def _object_hierarchy_parts( # pyrefly: ignore[bad-override] self, sig_node: addnodes.desc_signature ) -> tuple[str, ...]: return _parse_full_id(sig_node["bzl:object_id"]) + # ObjectDescription lacks type stubs, so @override triggers bad-override. @override - def _toc_entry_name(self, sig_node: addnodes.desc_signature) -> str: + def _toc_entry_name( # pyrefly: ignore[bad-override] + self, sig_node: addnodes.desc_signature + ) -> str: return sig_node["_toc_parts"][-1] def _get_object_type_display_name(self) -> str: - return self.env.get_domain(self.domain).object_types[self.objtype].lname + return self._get_bzl_domain().object_types[self.objtype].lname def _get_signature_object_type(self) -> str: return self._get_object_type_display_name() - def _get_alt_names(self, object_entry): - alt_names = [] + def _get_alt_names(self, object_entry: _ObjectEntry) -> list[str]: + alt_names: list[str] = [] full_id = object_entry.full_id label, _, symbol = full_id.partition("%") if symbol: @@ -947,7 +982,7 @@ def _get_signature_object_type(self) -> str: return "" @override - def _get_alt_names(self, object_entry): + def _get_alt_names(self, object_entry: _ObjectEntry) -> list[str]: alt_names = super()._get_alt_names(object_entry) _, _, symbol = object_entry.full_id.partition("%") # Allow refering to `mod_ext_name.tag_name`, even if the extension @@ -1230,7 +1265,7 @@ def _get_signature_object_type(self) -> str: return "" @override - def _get_alt_names(self, object_entry): + def _get_alt_names(self, object_entry: _ObjectEntry) -> list[str]: alt_names = super()._get_alt_names(object_entry) _, _, symbol = object_entry.full_id.partition("%") # Allow refering to `ProviderName.field`, even if the provider @@ -1249,23 +1284,26 @@ class _BzlTarget(_BzlObject): _TARGET_TYPE = _TargetType.TARGET - def handle_signature(self, sig_text, sig_node): - self._signature_add_object_type(sig_node) - if ":" in sig_text: - package, target_name = sig_text.split(":", 1) + @override + def handle_signature( + self, sig: str, signode: addnodes.desc_signature + ) -> _BzlObjectId: + self._signature_add_object_type(signode) + if ":" in sig: + package, target_name = sig.split(":", 1) else: - target_name = sig_text + target_name = sig package = self.env.ref_context["bzl:file"] package = package[: package.find(":BUILD")] package = package + ":" if self._TARGET_TYPE == _TargetType.FLAG: - sig_node += addnodes.desc_addname("--", "--") - sig_node += addnodes.desc_addname(package, package) - sig_node += addnodes.desc_name(target_name, target_name) + signode += addnodes.desc_addname("--", "--") + signode += addnodes.desc_addname(package, package) + signode += addnodes.desc_name(target_name, target_name) obj_id = _BzlObjectId.from_env(self.env, label=package + target_name) - sig_node["bzl:object_id"] = obj_id.full_id + signode["bzl:object_id"] = obj_id.full_id return obj_id @override @@ -1286,6 +1324,7 @@ class _BzlFlag(_BzlTarget): def _get_signature_object_type(self) -> str: return "flag" + @override def _get_additional_index_types(self): return ["target"] @@ -1427,7 +1466,7 @@ class _BzlIndex(domains.Index): shortname = "Bzl" def generate( - self, docnames: Iterable[str] = None + self, docnames: Iterable[str] | None = None ) -> tuple[list[tuple[str, list[domains.IndexEntry]]], bool]: content = collections.defaultdict(list) @@ -1607,22 +1646,26 @@ class _BzlDomain(domains.Domain): "alt_names": {}, } + # domains.Domain lacks type stubs, so @override triggers bad-override. @override - def get_full_qualified_name( + def get_full_qualified_name( # pyrefly: ignore[bad-override] self, node: docutils_nodes.Element - ) -> typing.Union[str, None]: + ) -> str | None: bzl_file = node.get("bzl:file") symbol_name = node.get("bzl:symbol") ref_target = node.get("reftarget") return ".".join(filter(None, [bzl_file, symbol_name, ref_target])) + # domains.Domain lacks type stubs, so @override triggers bad-override. @override - def get_objects(self) -> Iterable[_GetObjectsTuple]: - for entry in self.data["objects"].values(): + def get_objects(self) -> Iterable[_GetObjectsTuple]: # pyrefly: ignore[bad-override] + objects: dict[str, _ObjectEntry] = self.data["objects"] + for entry in objects.values(): yield entry.to_get_objects_tuple() + # domains.Domain lacks type stubs, so @override triggers bad-override. @override - def resolve_any_xref( + def resolve_any_xref( # pyrefly: ignore[bad-override] self, env: environment.BuildEnvironment, fromdocname: str, @@ -1644,8 +1687,9 @@ def resolve_any_xref( matches = [(f"bzl:{entry.object_type}", ref_node)] return matches + # domains.Domain lacks type stubs, so @override triggers bad-override. @override - def resolve_xref( + def resolve_xref( # pyrefly: ignore[bad-override] self, env: environment.BuildEnvironment, fromdocname: str, @@ -1654,7 +1698,7 @@ def resolve_xref( target: str, node: addnodes.pending_xref, contnode: docutils_nodes.Element, - ) -> typing.Union[docutils_nodes.Element, None]: + ) -> docutils_nodes.Element | None: _log_debug( "resolve_xref: fromdocname=%s, typ=%s, target=%s", fromdocname, typ, target ) @@ -1671,7 +1715,7 @@ def resolve_xref( def _find_entry_for_xref( self, fromdocname: str, object_type: str, target: str - ) -> typing.Union[_ObjectEntry, None]: + ) -> _ObjectEntry | None: if target.startswith("--"): target = target.strip("-") object_type = "flag" @@ -1742,8 +1786,7 @@ def add_object(self, entry: _ObjectEntry, alt_names=None) -> None: else: base_name = label.split(":")[-1] - if alt_names is not None: - alt_names = list(alt_names) + alt_names = list(alt_names) if alt_names else [] # Add the repo-less version as an alias alt_names.append(label + (f"%{symbol}" if symbol else "")) @@ -1755,8 +1798,9 @@ def add_object(self, entry: _ObjectEntry, alt_names=None) -> None: self.data["doc_names"].setdefault(docname, {}) self.data["doc_names"][docname][base_name] = entry + # domains.Domain lacks type stubs, so @override triggers bad-override. @override - def clear_doc(self, docname: str) -> None: + def clear_doc(self, docname: str) -> None: # pyrefly: ignore[bad-override] if docname not in self.data["doc_names"]: return for base_name, entry in self.data["doc_names"][docname].items(): @@ -1776,9 +1820,7 @@ def clear_doc(self, docname: str) -> None: del self.data["alt_names"][alt_name] del self.data["doc_names"][docname] - def merge_domaindata( - self, docnames: list[str], otherdata: dict[str, typing.Any] - ) -> None: + def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> None: # Merge in simple dict[key, value] data for top_key in ("objects",): self.data[top_key].update(otherdata.get(top_key, {})) @@ -1828,7 +1870,9 @@ def _on_missing_reference(app, env: environment.BuildEnvironment, node, contnode if new_target != original_target: # Access the intersphinx extension's internal mapping # we try to resolve the reference again with the stripped name - from sphinx.ext.intersphinx import missing_reference + from sphinx.ext.intersphinx import ( # pyrefly: ignore[missing-import] + missing_reference, + ) node["reftarget"] = new_target return missing_reference(app, env, node, contnode) diff --git a/sphinxdocs/tests/proto_to_markdown/BUILD.bazel b/sphinxdocs/tests/proto_to_markdown/BUILD.bazel index 632d6d946f..e1c358773c 100644 --- a/sphinxdocs/tests/proto_to_markdown/BUILD.bazel +++ b/sphinxdocs/tests/proto_to_markdown/BUILD.bazel @@ -19,6 +19,6 @@ py_test( srcs = ["proto_to_markdown_test.py"], deps = [ "//sphinxdocs/private:proto_to_markdown_lib", - "@dev_pip//absl_py", + "@pypi//absl_py", ], ) diff --git a/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py b/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py index d88d2bf127..753e8f7659 100644 --- a/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py +++ b/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py @@ -15,9 +15,13 @@ import io from absl.testing import absltest -from google.protobuf import text_format +from google.protobuf import ( # pyrefly: ignore[missing-source-for-stubs] + text_format, +) from sphinxdocs.private import proto_to_markdown -from stardoc.proto import stardoc_output_pb2 +from stardoc.proto import ( # pyrefly: ignore[missing-import] + stardoc_output_pb2, +) _EVERYTHING_MODULE = """\ module_docstring: "MODULE_DOC_STRING" diff --git a/sphinxdocs/tests/sphinx_build/BUILD.bazel b/sphinxdocs/tests/sphinx_build/BUILD.bazel index b9e77220df..ec0878d862 100644 --- a/sphinxdocs/tests/sphinx_build/BUILD.bazel +++ b/sphinxdocs/tests/sphinx_build/BUILD.bazel @@ -5,7 +5,7 @@ py_test( srcs = ["directory_syncer_test.py"], deps = [ "//sphinxdocs/private:sphinx_build_lib", - "@dev_pip//absl_py", - "@dev_pip//sphinx", + "@pypi//absl_py", + "@pypi//sphinx", ], ) diff --git a/sphinxdocs/tests/sphinx_docs/BUILD.bazel b/sphinxdocs/tests/sphinx_docs/BUILD.bazel index 4bbaf90691..71bc1f3d79 100644 --- a/sphinxdocs/tests/sphinx_docs/BUILD.bazel +++ b/sphinxdocs/tests/sphinx_docs/BUILD.bazel @@ -44,8 +44,8 @@ sphinx_build_binary( name = "sphinx-build", tags = ["manual"], # Only needed as part of sphinx doc building deps = [ - "@dev_pip//myst_parser", - "@dev_pip//sphinx", + "@pypi//myst_parser", + "@pypi//sphinx", ], ) @@ -58,5 +58,5 @@ py_test( name = "sphinx_docs_output_test", srcs = ["sphinx_docs_output_test.py"], data = [":docs"], - deps = ["@dev_pip//absl_py"], + deps = ["@pypi//absl_py"], ) diff --git a/sphinxdocs/tests/sphinx_docs_conf_in_other_dir/BUILD.bazel b/sphinxdocs/tests/sphinx_docs_conf_in_other_dir/BUILD.bazel index eecbb90897..78f7d5e4bc 100644 --- a/sphinxdocs/tests/sphinx_docs_conf_in_other_dir/BUILD.bazel +++ b/sphinxdocs/tests/sphinx_docs_conf_in_other_dir/BUILD.bazel @@ -27,8 +27,8 @@ sphinx_build_binary( name = "sphinx-build", tags = ["manual"], deps = [ - "@dev_pip//myst_parser", - "@dev_pip//sphinx", + "@pypi//myst_parser", + "@pypi//sphinx", ], ) diff --git a/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel b/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel index 2cbc773f77..ffc9697e7f 100644 --- a/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel +++ b/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel @@ -94,9 +94,9 @@ sphinx_build_binary( tags = ["manual"], # Only needed as part of sphinx doc building deps = [ "//sphinxdocs/src/sphinx_bzl", - "@dev_pip//myst_parser", - "@dev_pip//sphinx", - "@dev_pip//typing_extensions", # Needed by sphinx_stardoc + "@pypi//myst_parser", + "@pypi//sphinx", + "@pypi//typing_extensions", # Needed by sphinx_stardoc ], ) @@ -104,5 +104,5 @@ py_test( name = "sphinx_output_test", srcs = ["sphinx_output_test.py"], data = [":docs"], - deps = ["@dev_pip//absl_py"], + deps = ["@pypi//absl_py"], ) diff --git a/sphinxdocs/tests/support/pyrefly/BUILD.bazel b/sphinxdocs/tests/support/pyrefly/BUILD.bazel new file mode 100644 index 0000000000..447af06f8f --- /dev/null +++ b/sphinxdocs/tests/support/pyrefly/BUILD.bazel @@ -0,0 +1,8 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +package(default_visibility = ["//:__subpackages__"]) + +bzl_library( + name = "pyrefly", + srcs = ["pyrefly.bzl"], +) diff --git a/sphinxdocs/tests/support/pyrefly/pyrefly.bzl b/sphinxdocs/tests/support/pyrefly/pyrefly.bzl new file mode 100644 index 0000000000..ac0d7ea331 --- /dev/null +++ b/sphinxdocs/tests/support/pyrefly/pyrefly.bzl @@ -0,0 +1,5 @@ +"""Aspect definition for Pyrefly static type checking.""" + +load("@rules_pyrefly//pyrefly:pyrefly.bzl", "pyrefly") + +pyrefly_aspect = pyrefly() diff --git a/tests/bootstrap_impls/bazel_tools_importable_test.py b/tests/bootstrap_impls/bazel_tools_importable_test.py index c374dd5dcf..7445100dda 100644 --- a/tests/bootstrap_impls/bazel_tools_importable_test.py +++ b/tests/bootstrap_impls/bazel_tools_importable_test.py @@ -5,9 +5,9 @@ class BazelToolsImportableTest(unittest.TestCase): def test_bazel_tools_importable(self): try: - import bazel_tools - import bazel_tools.tools.python - import bazel_tools.tools.python.runfiles # noqa: F401 + import bazel_tools # pyrefly: ignore[missing-import] + import bazel_tools.tools.python # pyrefly: ignore[missing-import] + import bazel_tools.tools.python.runfiles # pyrefly: ignore[missing-import] # noqa: F401 except ImportError as exc: raise AssertionError( "Failed to import bazel_tools.python.runfiles\n" diff --git a/tests/bootstrap_impls/bin.py b/tests/bootstrap_impls/bin.py index 3d467dcf29..0713b5f1be 100644 --- a/tests/bootstrap_impls/bin.py +++ b/tests/bootstrap_impls/bin.py @@ -23,4 +23,4 @@ print("sys.flags.safe_path:", sys.flags.safe_path) print("file:", __file__) print("sys.executable:", sys.executable) -print("sys._base_executable:", sys._base_executable) +print("sys._base_executable:", getattr(sys, "_base_executable", None)) diff --git a/tests/bootstrap_impls/sys_path_order_test.py b/tests/bootstrap_impls/sys_path_order_test.py index a9018c39ce..d55a93528b 100644 --- a/tests/bootstrap_impls/sys_path_order_test.py +++ b/tests/bootstrap_impls/sys_path_order_test.py @@ -67,7 +67,7 @@ def test_sys_path_order(self): f"{i}: ({category}) {value}" for i, (category, value) in enumerate(categorized_paths) ) - if None in (last_stdlib, first_user, first_runtime_site): + if last_stdlib is None or first_user is None or first_runtime_site is None: self.fail( "Failed to find position for one of:\n" + f"{last_stdlib=} {first_user=} {first_runtime_site=}\n" diff --git a/tests/build_data/build_data_test.py b/tests/build_data/build_data_test.py index 6be4e52c84..69f2e48e33 100644 --- a/tests/build_data/build_data_test.py +++ b/tests/build_data/build_data_test.py @@ -5,7 +5,7 @@ class BuildDataTest(unittest.TestCase): def test_target_build_data(self): - import bazel_binary_info + import bazel_binary_info # pyrefly: ignore[missing-import] self.assertIn("build_data.txt", bazel_binary_info.BUILD_DATA_FILE) @@ -19,7 +19,9 @@ def test_target_build_data(self): def test_tool_build_data(self): rf = runfiles.Create() + assert rf is not None # type assert path = rf.Rlocation("rules_python/tests/build_data/tool_build_data.txt") + assert path is not None # type assert with open(path) as fp: build_data = fp.read() diff --git a/tests/build_data/print_build_data.py b/tests/build_data/print_build_data.py index 0af77d72be..54d2d45361 100644 --- a/tests/build_data/print_build_data.py +++ b/tests/build_data/print_build_data.py @@ -1,3 +1,3 @@ -import bazel_binary_info +import bazel_binary_info # pyrefly: ignore[missing-import] print(bazel_binary_info.get_build_data()) diff --git a/tests/cc/current_py_cc_headers/abi3_headers_linkage_test.py b/tests/cc/current_py_cc_headers/abi3_headers_linkage_test.py index 2d64828278..1eb229d29c 100644 --- a/tests/cc/current_py_cc_headers/abi3_headers_linkage_test.py +++ b/tests/cc/current_py_cc_headers/abi3_headers_linkage_test.py @@ -10,9 +10,11 @@ class CheckLinkageTest(unittest.TestCase): @unittest.skipUnless(sys.platform.startswith("win"), "requires windows") def test_linkage_windows(self): rf = runfiles.Create() + assert rf is not None # type assert dll_path = rf.Rlocation( "rules_python/tests/cc/current_py_cc_headers/bin_abi3.dll" ) + assert dll_path is not None # type assert pe = pefile.PE(dll_path) if not hasattr(pe, "DIRECTORY_ENTRY_IMPORT"): self.fail("No import directory found.") diff --git a/tests/cc/py_extension/py_extension_pkg_test.py b/tests/cc/py_extension/py_extension_pkg_test.py index e3176d6a6c..68c6dc3ee6 100644 --- a/tests/cc/py_extension/py_extension_pkg_test.py +++ b/tests/cc/py_extension/py_extension_pkg_test.py @@ -1,6 +1,8 @@ import unittest -from tests.cc.py_extension import ext_pkg_test +from tests.cc.py_extension import ( + ext_pkg_test, # pyrefly: ignore[missing-module-attribute] +) class PyExtensionPkgTest(unittest.TestCase): @@ -9,7 +11,7 @@ def test_import_via_package(self): def test_direct_import(self): with self.assertRaises(ModuleNotFoundError): - import ext_pkg_test # buildifier: disable=g-import-not-at-top # noqa: F401 + import ext_pkg_test # pyrefly: ignore[missing-import] # noqa: F401 if __name__ == "__main__": diff --git a/tests/cc/py_extension/py_extension_test.py b/tests/cc/py_extension/py_extension_test.py index d82fe22bcc..7ffcdd3e66 100644 --- a/tests/cc/py_extension/py_extension_test.py +++ b/tests/cc/py_extension/py_extension_test.py @@ -2,7 +2,7 @@ import sys import unittest -import ext_shared +import ext_shared # pyrefly: ignore[missing-import] from elftools.elf.dynamic import DynamicSection from elftools.elf.elffile import ELFFile @@ -26,9 +26,9 @@ def test_inspect_elf(self): self.assertTrue(isinstance(dynamic_section, DynamicSection)) needed_libs = [ - tag.needed + tag.needed # pyrefly: ignore[missing-attribute] for tag in dynamic_section.iter_tags() - if tag.entry.d_tag == "DT_NEEDED" + if tag.entry.d_tag == "DT_NEEDED" # pyrefly: ignore[missing-attribute] ] self.assertIn("libadd_one_shared.so", needed_libs) diff --git a/tests/entry_points/py_console_script_gen_test.py b/tests/entry_points/py_console_script_gen_test.py index 92fa42f167..54e86bb671 100644 --- a/tests/entry_points/py_console_script_gen_test.py +++ b/tests/entry_points/py_console_script_gen_test.py @@ -162,7 +162,7 @@ def test_a_single_entry_point(self): raise if __name__ == "__main__": - sys.exit(baz()) # type: ignore + sys.exit(baz()) # pyrefly: ignore[not-callable] """ ) self.assertEqual(want, got) diff --git a/tests/integration/runner.py b/tests/integration/runner.py index c187623b3c..9efcbebb89 100644 --- a/tests/integration/runner.py +++ b/tests/integration/runner.py @@ -103,19 +103,19 @@ def run_bazel(self, *args: str, check: bool = True) -> ExecuteResult: Returns: An `ExecuteResult` from running Bazel """ - args = [str(self.bazel), *args] + cmd_args = [str(self.bazel), *args] env = self.bazel_env - _logger.info("executing: %s", shlex.join(args)) + _logger.info("executing: %s", shlex.join(cmd_args)) cwd = self.repo_root proc_result = subprocess.run( - args=args, + args=cmd_args, text=True, capture_output=True, cwd=cwd, env=env, check=False, ) - exec_result = ExecuteResult(args, env, cwd, proc_result) + exec_result = ExecuteResult(cmd_args, env, cwd, proc_result) if check and exec_result.exit_code: raise ExecuteError(exec_result) else: diff --git a/tests/integration/uv_lock_pypi_server.py b/tests/integration/uv_lock_pypi_server.py index 0d940e7569..1f350b809f 100644 --- a/tests/integration/uv_lock_pypi_server.py +++ b/tests/integration/uv_lock_pypi_server.py @@ -118,7 +118,7 @@ def main(): app = app_from_config(config) app = setup_routes_from_config(app, config) - server = make_server(args.host, args.port, app) + server = make_server(args.host, args.port, app) # pyrefly: ignore[bad-argument-type] port = server.server_address[1] base_url = "http://{}:{}".format(args.host, port) diff --git a/tests/multi_pypi/pypi_alpha/pypi_alpha_test.py b/tests/multi_pypi/pypi_alpha/pypi_alpha_test.py index 0521327563..ff0561a6f4 100644 --- a/tests/multi_pypi/pypi_alpha/pypi_alpha_test.py +++ b/tests/multi_pypi/pypi_alpha/pypi_alpha_test.py @@ -1,6 +1,8 @@ import sys -from more_itertools import __version__ +from more_itertools import ( + __version__, # pyrefly: ignore[missing-module-attribute] +) if __name__ == "__main__": expected_version = "9.1.0" diff --git a/tests/multi_pypi/pypi_beta/pypi_beta_test.py b/tests/multi_pypi/pypi_beta/pypi_beta_test.py index 8c34de0735..bbb50dd8a8 100644 --- a/tests/multi_pypi/pypi_beta/pypi_beta_test.py +++ b/tests/multi_pypi/pypi_beta/pypi_beta_test.py @@ -1,6 +1,8 @@ import sys -from more_itertools import __version__ +from more_itertools import ( + __version__, # pyrefly: ignore[missing-module-attribute] +) if __name__ == "__main__": expected_version = "9.0.0" diff --git a/tests/news/news_test.py b/tests/news/news_test.py index a8ed7a2849..66476145a2 100644 --- a/tests/news/news_test.py +++ b/tests/news/news_test.py @@ -6,6 +6,7 @@ def _get_news_dir(): rf = runfiles.Create() + assert rf is not None # type assert path = rf.Rlocation("rules_python/news") if path: return pathlib.Path(path) diff --git a/tests/py_zipapp/BUILD.bazel b/tests/py_zipapp/BUILD.bazel index a68448d964..e0f878cbe1 100644 --- a/tests/py_zipapp/BUILD.bazel +++ b/tests/py_zipapp/BUILD.bazel @@ -17,6 +17,8 @@ py_binary( }, }), main = "main.py", + # Pyrefly's bazel-check validator rejects explicit import paths with '.' components from :bin_deps. + tags = ["no-pyrefly"], deps = [":bin_deps"], ) @@ -62,6 +64,8 @@ py_binary( "//python/config_settings:venvs_site_packages": "no", }, main = "main.py", + # Pyrefly's bazel-check validator rejects explicit import paths with '.' components from :bin_deps. + tags = ["no-pyrefly"], deps = [":bin_deps"], ) @@ -106,6 +110,8 @@ py_library( srcs = ["some_dep.py"], experimental_venvs_site_packages = "//python/config_settings:venvs_site_packages", imports = ["."], + # Pyrefly's bazel-check validator rejects explicit import paths with '.' components. + tags = ["no-pyrefly"], ) py_library( diff --git a/tests/repl/BUILD.bazel b/tests/repl/BUILD.bazel index b3986cc023..8fc239a06a 100644 --- a/tests/repl/BUILD.bazel +++ b/tests/repl/BUILD.bazel @@ -26,6 +26,7 @@ py_reconfig_test( }, main = "repl_test.py", python_version = "3.12", + deps = ["//python/runfiles"], ) py_reconfig_test( @@ -41,4 +42,5 @@ py_reconfig_test( main = "repl_test.py", python_version = "3.12", repl_dep = ":helper/test_module", + deps = ["//python/runfiles"], ) diff --git a/tests/repl/repl_test.py b/tests/repl/repl_test.py index 2b3d5c7a4d..76b407b49e 100644 --- a/tests/repl/repl_test.py +++ b/tests/repl/repl_test.py @@ -3,12 +3,12 @@ import sys # noqa: F401 import tempfile import unittest +from collections.abc import Iterable from pathlib import Path -from typing import Iterable -from python import runfiles +from python.runfiles import runfiles -rfiles = runfiles.Create() +rfiles = runfiles.CreateOrRaise() # Signals the tests below whether we should be expecting the import of # helpers/test_module.py on the REPL to work or not. @@ -29,10 +29,11 @@ def setUp(self): rpath = "rules_python/python/bin/repl" if IS_WINDOWS: rpath += ".exe" - self.repl = rfiles.Rlocation(rpath) - assert self.repl + repl = rfiles.Rlocation(rpath) + assert repl is not None, f"Could not find {rpath}" # type assert if IS_WINDOWS: - self.repl = os.path.normpath(self.repl) + repl = os.path.normpath(repl) + self.repl: str = repl def run_code_in_repl(self, lines: Iterable[str], *, env=None) -> str: """Runs the lines of code in the REPL and returns the text output.""" @@ -89,7 +90,7 @@ def test_repl_version(self): def test_cannot_import_test_module_directly(self): """Validates that we cannot import helper/test_module.py since it's not a direct dep.""" with self.assertRaises(ModuleNotFoundError): - import test_module # noqa: F401 + import test_module # pyrefly: ignore[missing-import] # noqa: F401 @unittest.skipIf( not EXPECT_TEST_MODULE_IMPORTABLE, "test only works without repl_dep set" diff --git a/tests/runfiles/pathlib_test.py b/tests/runfiles/pathlib_test.py index a959138235..5aefc4f4d3 100644 --- a/tests/runfiles/pathlib_test.py +++ b/tests/runfiles/pathlib_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import pathlib import tempfile @@ -25,7 +27,7 @@ def setUp(self) -> None: def _create_runfiles(self) -> runfiles.Runfiles: r = runfiles.Create({"RUNFILES_DIR": self.root_dir}) - assert r is not None + assert r is not None # type assert return r def tearDown(self) -> None: diff --git a/tests/runfiles/runfiles_test.py b/tests/runfiles/runfiles_test.py index 38a89ede7e..ce74a3d4ac 100644 --- a/tests/runfiles/runfiles_test.py +++ b/tests/runfiles/runfiles_test.py @@ -12,12 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations + import json import os import pathlib import tempfile import unittest -from typing import Any, List, Optional +from typing import Any from python.runfiles import runfiles from python.runfiles.runfiles import _RepositoryMapping @@ -29,9 +31,9 @@ class RunfilesTest(unittest.TestCase): def testRlocationArgumentValidation(self) -> None: r = runfiles.Create({"RUNFILES_DIR": "whatever"}) assert r is not None # mypy doesn't understand the unittest api. - self.assertRaises(ValueError, lambda: r.Rlocation(None)) # type: ignore + self.assertRaises(ValueError, lambda: r.Rlocation(None)) # type: ignore[arg-type] # pyrefly: ignore[bad-argument-type] self.assertRaises(ValueError, lambda: r.Rlocation("")) - self.assertRaises(TypeError, lambda: r.Rlocation(1)) # type: ignore + self.assertRaises(TypeError, lambda: r.Rlocation(1)) # type: ignore[arg-type] # pyrefly: ignore[bad-argument-type] self.assertRaisesRegex( ValueError, "is not normalized", lambda: r.Rlocation("../foo") ) @@ -71,7 +73,7 @@ def testRlocationWithData(self) -> None: settings_path = r.Rlocation( "rules_python/tests/support/current_build_settings.json" ) - assert settings_path is not None + assert settings_path is not None # type assert settings = json.loads(pathlib.Path(settings_path).read_text()) self.assertIn("bootstrap_impl", settings) @@ -771,11 +773,11 @@ def IsWindows() -> bool: class _MockFile: def __init__( - self, name: Optional[str] = None, contents: Optional[List[Any]] = None + self, name: str | None = None, contents: list[Any] | None = None ) -> None: self._contents = contents or [] self._name = name or "x" - self._path: Optional[str] = None + self._path: str | None = None def __enter__(self) -> Any: tmpdir = os.environ.get("TEST_TMPDIR") @@ -795,7 +797,7 @@ def __exit__( os.rmdir(os.path.dirname(self._path)) def Path(self) -> str: - assert self._path is not None + assert self._path is not None # type assert return self._path diff --git a/tests/runtime_env_toolchain/toolchain_runs_test.py b/tests/runtime_env_toolchain/toolchain_runs_test.py index 13b5775ff0..f3dcee3786 100644 --- a/tests/runtime_env_toolchain/toolchain_runs_test.py +++ b/tests/runtime_env_toolchain/toolchain_runs_test.py @@ -1,5 +1,4 @@ import json -import pathlib import platform import sys import unittest @@ -9,11 +8,11 @@ class RunTest(unittest.TestCase): def test_ran(self): - rf = runfiles.Create() - settings_path = rf.Rlocation( - "rules_python/tests/support/current_build_settings.json" + rf = runfiles.CreateOrRaise() + settings_path = ( + rf.root() / "rules_python/tests/support/current_build_settings.json" ) - settings = json.loads(pathlib.Path(settings_path).read_text()) + settings = json.loads(settings_path.read_text()) if platform.system() == "Windows": self.assertEqual( diff --git a/tests/support/pyrefly/pyrefly.bzl b/tests/support/pyrefly/pyrefly.bzl index bcb791403f..ac0d7ea331 100644 --- a/tests/support/pyrefly/pyrefly.bzl +++ b/tests/support/pyrefly/pyrefly.bzl @@ -2,6 +2,4 @@ load("@rules_pyrefly//pyrefly:pyrefly.bzl", "pyrefly") -pyrefly_aspect = pyrefly( - opt_in_tags = ["pyrefly"], -) +pyrefly_aspect = pyrefly() diff --git a/tests/support/pytest_test/pytest_bootstrap_template.py b/tests/support/pytest_test/pytest_bootstrap_template.py index 9769531f47..2587353306 100644 --- a/tests/support/pytest_test/pytest_bootstrap_template.py +++ b/tests/support/pytest_test/pytest_bootstrap_template.py @@ -1,6 +1,6 @@ import sys -import pytest_bazel +import pytest_bazel # pyrefly: ignore[missing-import] TEST_FILES = """%TEST_FILES%""".splitlines() diff --git a/tests/toolchains/python_toolchain_test.py b/tests/toolchains/python_toolchain_test.py index ff45fc0863..dcd2438cd7 100644 --- a/tests/toolchains/python_toolchain_test.py +++ b/tests/toolchains/python_toolchain_test.py @@ -13,9 +13,11 @@ def test_expected_toolchain_matches(self): expect_version = os.environ["EXPECT_PYTHON_VERSION"] rf = runfiles.Create() + assert rf is not None # type assert settings_path = rf.Rlocation( "rules_python/tests/support/current_build_settings.json" ) + assert settings_path is not None # type assert settings = json.loads(pathlib.Path(settings_path).read_text()) expected = "python_{}".format(expect_version.replace(".", "_")) diff --git a/tests/tools/private/release/BUILD.bazel b/tests/tools/private/release/BUILD.bazel index dcef2ab53a..dc02394960 100644 --- a/tests/tools/private/release/BUILD.bazel +++ b/tests/tools/private/release/BUILD.bazel @@ -16,11 +16,13 @@ py_library( py_library( name = "release_test_helper", + testonly = True, srcs = ["release_test_helper.py"], target_compatible_with = NOT_WINDOWS, deps = [ "//tools/private/release:mock_gh", "//tools/private/release:release_lib", + "@pypi//pytest", ], ) diff --git a/tests/tools/private/release/git_test.py b/tests/tools/private/release/git_test.py index 4a8cada4cc..e39787f187 100644 --- a/tests/tools/private/release/git_test.py +++ b/tests/tools/private/release/git_test.py @@ -10,7 +10,7 @@ @pytest.fixture(name="git_obj") def fixture_git_obj(mocker): git = Git(".") - git.mock_run_git = mocker.patch.object(git, "_run_git") + git.mock_run_git = mocker.patch.object(git, "_run_git") # pyrefly: ignore[missing-attribute] return git diff --git a/tests/uv/lock/lock_run_test.py b/tests/uv/lock/lock_run_test.py index 6de5a96378..2de9147d99 100644 --- a/tests/uv/lock/lock_run_test.py +++ b/tests/uv/lock/lock_run_test.py @@ -7,6 +7,7 @@ from python import runfiles rfiles = runfiles.Create() +assert rfiles is not None, "Failed to create runfiles" def _relative_rpath(path: str) -> Path: diff --git a/tests/venv_site_packages_libs/shared_lib_loading_test.py b/tests/venv_site_packages_libs/shared_lib_loading_test.py index a3f7bfcd5a..aa440e4053 100644 --- a/tests/venv_site_packages_libs/shared_lib_loading_test.py +++ b/tests/venv_site_packages_libs/shared_lib_loading_test.py @@ -6,13 +6,13 @@ # Optional imports for ELF/Mach-O analysis if os.name == "posix" and sys.platform != "darwin": - from elftools.elf.elffile import ELFFile + from elftools.elf.elffile import ELFFile # pyrefly: ignore[missing-import] else: ELFFile = None if sys.platform == "darwin": - from macholib import mach_o - from macholib.MachO import MachO + from macholib import mach_o # pyrefly: ignore[missing-import] + from macholib.MachO import MachO # pyrefly: ignore[missing-import] else: mach_o = None MachO = None @@ -36,7 +36,7 @@ def setUp(self): @unittest.skipIf(os.name == "nt", "Tests Unix-specific extension loading") def test_shared_library_linking_unix(self): try: - import ext_with_libs.adder + import ext_with_libs.adder # pyrefly: ignore[missing-import] except ImportError as e: spec = importlib.util.find_spec("ext_with_libs.adder") if not spec or not spec.origin: @@ -75,7 +75,7 @@ def test_shared_library_linking_unix(self): def test_shared_library_loading_windows(self): # We import markupsafe._speedups (a .cp311-win_amd64.pyd extension) try: - import markupsafe._speedups + import markupsafe._speedups # pyrefly: ignore[missing-import] module = markupsafe._speedups except ImportError as e: @@ -120,30 +120,32 @@ def _get_linking_info(self, path): def _get_elf_info(self, path): """Extracts linking information from an ELF file.""" + assert ELFFile is not None # type assert info = {"rpaths": [], "needed": [], "undefined_symbols": []} with open(path, "rb") as f: elf = ELFFile(f) dynamic = elf.get_section_by_name(".dynamic") if dynamic: for tag in dynamic.iter_tags(): - if tag.entry.d_tag == "DT_NEEDED": - info["needed"].append(tag.needed) - elif tag.entry.d_tag == "DT_RPATH": - info["rpaths"].append(tag.rpath) - elif tag.entry.d_tag == "DT_RUNPATH": - info["rpaths"].append(tag.runpath) + if tag.entry.d_tag == "DT_NEEDED": # pyrefly: ignore[missing-attribute] + info["needed"].append(tag.needed) # pyrefly: ignore[missing-attribute] + elif tag.entry.d_tag == "DT_RPATH": # pyrefly: ignore[missing-attribute] + info["rpaths"].append(tag.rpath) # pyrefly: ignore[missing-attribute] + elif tag.entry.d_tag == "DT_RUNPATH": # pyrefly: ignore[missing-attribute] + info["rpaths"].append(tag.runpath) # pyrefly: ignore[missing-attribute] dynsym = elf.get_section_by_name(".dynsym") if dynsym: info["undefined_symbols"] = [ s.name for s in dynsym.iter_symbols() - if s.entry["st_shndx"] == "SHN_UNDEF" + if s.entry["st_shndx"] == "SHN_UNDEF" # pyrefly: ignore[missing-attribute] ] return info def _get_macho_info(self, path): """Extracts linking information from a Mach-O file.""" + assert MachO is not None and mach_o is not None # type assert info = {"rpaths": [], "needed": []} macho = MachO(path) for header in macho.headers: diff --git a/tools/private/release/mock_gh.py b/tools/private/release/mock_gh.py index e5def53799..0b5b672517 100644 --- a/tools/private/release/mock_gh.py +++ b/tools/private/release/mock_gh.py @@ -158,4 +158,5 @@ def get_pr_comments(self, pr_num: int) -> list[dict]: return self.pr_comments.get(pr_num, []) def get_merge_commits_for_prs(self, pending_items: list) -> list: + # pyrefly: ignore[bad-argument-type] return resolve_merge_commits_for_prs(self, pending_items) diff --git a/tools/private/release/process_backports.py b/tools/private/release/process_backports.py index 27eb3226b1..4ad846c0ac 100644 --- a/tools/private/release/process_backports.py +++ b/tools/private/release/process_backports.py @@ -420,7 +420,7 @@ def _run_internal(self) -> int: body = self.gh.get_issue_body(args.issue) if args.add: - items_to_add = [] + items_to_add: list[dict[str, Any]] = [] for pr_ref in args.add: try: pr_num = self.gh.resolve_pr_number(pr_ref) diff --git a/tools/private/release/promote.py b/tools/private/release/promote.py index 560c531383..44ccd5ba52 100644 --- a/tools/private/release/promote.py +++ b/tools/private/release/promote.py @@ -77,9 +77,10 @@ def run(self) -> int: if not latest_rc: print(f"Error: No release candidate tags found matching {version}-rc*") return 1 - commit_sha = self.git.get_commit_sha(latest_rc) + rc_commit_sha = self.git.get_commit_sha(latest_rc) else: latest_rc = None + rc_commit_sha = None # Verify issue can be found and read it early print(f"Verifying tracking issue #{issue_num} format...") @@ -102,16 +103,17 @@ def run(self) -> int: return 1 if is_first_release: - if commit_sha != branch_sha: + assert rc_commit_sha is not None # type assert + if rc_commit_sha != branch_sha: print( - f"Error: The latest RC tag {latest_rc} ({commit_sha[:8]}) is not at" + f"Error: The latest RC tag {latest_rc} ({rc_commit_sha[:8]}) is not at" f" the head of release branch {remote_branch} ({branch_sha[:8]})." ) metadata = { "status": "error-rc-tag-not-branch-head", "rc": latest_rc, "branch_commit": branch_sha[:8], - "tag_commit": commit_sha[:8], + "tag_commit": rc_commit_sha[:8], } try: updated_body = update_task_in_body( @@ -130,6 +132,7 @@ def run(self) -> int: f" error status." ) return 1 + commit_sha = rc_commit_sha else: # Patch release: tag branch head directly commit_sha = branch_sha diff --git a/tools/private/release/release_issue.py b/tools/private/release/release_issue.py index 220348bd47..9c73d653d5 100644 --- a/tools/private/release/release_issue.py +++ b/tools/private/release/release_issue.py @@ -1,4 +1,5 @@ import re +from typing import Any class BackportTask: @@ -261,7 +262,7 @@ def parse_backports(body): return items -def add_backports_to_body(body: str, items: list[dict]) -> str: +def add_backports_to_body(body: str, items: list[dict[str, Any]]) -> str: """Adds new backport checklist items to the ## Backports section. Args: diff --git a/tools/private/update_deps/args.py b/tools/private/update_deps/args.py index 293294c370..610b1abc72 100644 --- a/tools/private/update_deps/args.py +++ b/tools/private/update_deps/args.py @@ -28,7 +28,11 @@ def path_from_runfiles(input: str) -> pathlib.Path: Returns: the pathlib.Path path to a file which is verified to exist. """ - path = pathlib.Path(runfiles.Create().Rlocation(input)) + rf = runfiles.Create() + assert rf is not None # type assert + rlocation_path = rf.Rlocation(input) + assert rlocation_path is not None # type assert + path = pathlib.Path(rlocation_path) if not path.exists(): raise ValueError(f"Path '{path}' does not exist") diff --git a/tools/private/update_deps/update_coverage_deps.py b/tools/private/update_deps/update_coverage_deps.py index 8a4ccb41ba..74ac657bad 100755 --- a/tools/private/update_deps/update_coverage_deps.py +++ b/tools/private/update_deps/update_coverage_deps.py @@ -111,8 +111,8 @@ def _map( filename: str, python_version: str, url: str, - digests: list, - platform: str, + digests: dict[str, str], + platform: str | tuple[str, str], **kwargs: Any, ): if platform and platform not in _supported_platforms: diff --git a/tools/private/update_deps/update_pip_deps.py b/tools/private/update_deps/update_pip_deps.py index 406697bc4d..9951a7abbb 100755 --- a/tools/private/update_deps/update_pip_deps.py +++ b/tools/private/update_deps/update_pip_deps.py @@ -27,7 +27,7 @@ import textwrap from dataclasses import dataclass -from pip._internal.cli.main import main as pip_main +from pip._internal.cli.main import main as pip_main # pyrefly: ignore[missing-import] from tools.private.update_deps.args import path_from_runfiles from tools.private.update_deps.update_file import update_file diff --git a/tools/wheelmaker.py b/tools/wheelmaker.py index 70e375b4ee..483e8fcefe 100644 --- a/tools/wheelmaker.py +++ b/tools/wheelmaker.py @@ -24,6 +24,7 @@ import stat import sys import zipfile +from collections.abc import Sequence from pathlib import Path _ZIP_EPOCH = (1980, 1, 1, 0, 0, 0) @@ -101,7 +102,7 @@ def normalize_pep440(version): def arcname_from( name: str, distribution_prefix: str, - strip_path_prefixes: Sequence[str] = (), # noqa: F821 + strip_path_prefixes: Sequence[str] = (), add_path_prefix: str = "", ) -> str: """Return the within-archive name for a given file path name. @@ -287,7 +288,12 @@ def __init__( self._wheelname_fragment_distribution_name + "-" + self._version ) - self._whlfile = None + self._whlfile: _WhlFile | None = None + + @property + def whlfile(self) -> _WhlFile: + assert self._whlfile is not None # type assert + return self._whlfile def __enter__(self): self._whlfile = _WhlFile( @@ -303,8 +309,9 @@ def __enter__(self): return self def __exit__(self, type, value, traceback): - self._whlfile.close() - self._whlfile = None + if self._whlfile is not None: + self._whlfile.close() + self._whlfile = None def wheelname(self) -> str: components = [ @@ -325,14 +332,14 @@ def disttags(self): return ["-".join([self._python_tag, self._abi, self._platform])] def distinfo_path(self, basename): - return self._whlfile.distinfo_path(basename) + return self.whlfile.distinfo_path(basename) def data_path(self, basename): - return self._whlfile.data_path(basename) + return self.whlfile.data_path(basename) def add_file(self, package_filename, real_filename): """Add given file to the distribution.""" - self._whlfile.add_file(package_filename, real_filename) + self.whlfile.add_file(package_filename, real_filename) def add_wheelfile(self): """Write WHEEL file to the distribution""" @@ -344,7 +351,7 @@ def add_wheelfile(self): """.format("true" if self._platform == "any" else "false") for tag in self.disttags(): wheel_contents += "Tag: %s\n" % tag - self._whlfile.add_string(self.distinfo_path("WHEEL"), wheel_contents) + self.whlfile.add_string(self.distinfo_path("WHEEL"), wheel_contents) def add_metadata(self, metadata, name, description): """Write METADATA file to the distribution.""" @@ -356,11 +363,11 @@ def add_metadata(self, metadata, name, description): # provided. metadata += description if description else "UNKNOWN" metadata += "\n" - self._whlfile.add_string(self.distinfo_path("METADATA"), metadata) + self.whlfile.add_string(self.distinfo_path("METADATA"), metadata) def add_recordfile(self): """Write RECORD file to the distribution.""" - self._whlfile.add_recordfile() + self.whlfile.add_recordfile() def get_files_to_package(input_files): @@ -548,7 +555,7 @@ def parse_args() -> argparse.Namespace: return parser.parse_args(sys.argv[1:]) -def _parse_file_pairs(content: List[str]) -> List[List[str]]: # noqa: F821 +def _parse_file_pairs(content: list[str]) -> list[list[str]]: """ Parse ; delimited lists of files into a 2D list. """